[Solved] Deformation in bone structure on second model additoin (three.js/mmd)

After quite a bit tinkering, I found out how to add a second (also animated) character via three.js
and mmd(helper/animator):

However it appears this second character will only work well if it has the SAME bone structure, height etc. as the first.
As soon as I choose a different one, there will be

a) deformations (that would please satan)

or

b) straight up bone binding errors (that halt all progess)

three.js:42519 THREE.PropertyBinding: Trying to bind to objectIndex of objectName, but is undefined. PropertyBinding {path: '.bones[左中指先].position', parsedPath: {…}, node: SkinnedMesh, rootNode: SkinnedMesh, getValue: ƒ, …} (283) [Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, Bone, …]
bind @ three.js:42519
getValue_unbound @ three.js:42398
saveOriginalState @ three.js:41893
_activateAction @ three.js:43858
play @ three.js:43133
_setupMeshAnimation @ MMDAnimationHelper.js:439
_addMesh @ MMDAnimationHelper.js:296
add @ MMDAnimationHelper.js:90
VmdControl @ main.js:245
eval @ VM447:25
onclick @ index.html?pmx2=YusakuFujiki/yusaku.pmx&stage=/livestageclub/livestageclubanimated.pmx:128
three.js:42519 THREE.PropertyBinding: Trying to bind to objectIndex of objectName, but is undefined. PropertyBinding {path: '.bones[左中指先].quaternion', parsedPath: {…}, node: SkinnedMesh, rootNode: SkinnedMesh, getValue: ƒ, …}

(Here, mesh2 has 283 bones. mesh 1 130 …)

I suspect that the animation (vmd) is applied to both pmx and pmx2 without rhyme or reason.

How can this be done in a way that accomodates both models individual chracteristics ?

Note:
All animations play well when only applied to a single model - no matter which.
Only the introduction of a second model poses issues.

Here is my code


let scene, renderer, camera, mesh, mesh2, 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 pmx2 = getQueryStringValue('pmx2');
const stage = getQueryStringValue('stage') || "/sorclair/sorclair.pmx";
const light = getQueryStringValue('light');
let Pmx;
let Pmx2;
let StagePath = stage ? `./stages/${stage.trim()}` : './stages/sorclair/sorclair.pmx';


if (pmx) {
  Pmx = `./pmx/pronama/${pmx.trim()}`;
  console.log(`PMX: ${pmx.trim()}`);
} else {
  console.log("No PMX selected.");
}
if (pmx2) {
  Pmx2 = `./pmx/pronama/${pmx2.trim()}`;
  console.log(`PMX2: ${pmx2.trim()}`);
} else {
  console.log("No PMX2 selected.");
}
if (StagePath) {
  StagePath = `./stages${stage.trim()}`;
} else {
  StagePath = './stages/sorclair/sorclair.pmx';
}
console.log('StagePath:', StagePath);
if (StagePath) {
  const loader = new THREE.MMDLoader();
  const lastIndex = StagePath.lastIndexOf("/");
  const basePath = StagePath.substring(0, lastIndex);
  const vmd1Path = `${basePath}/001.vmd`;
  const vmd2Path = `${basePath}/002.vmd`;
  loader.load(StagePath, (stageObject) => {
    var ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
    scene.add(ambientLight);
    scene.add(stageObject);
    const mixer = new THREE.AnimationMixer(stageObject);
    loader.loadAnimation(vmd1Path, stageObject, (vmd1Clip) => {
      vmd1Clip.name = "001";
      console.log(`Loaded VMD: ${vmd1Path}`);
      const motionObject1 = MotionObjects.find(obj => obj.id === "001");
      if (motionObject1) {
        motionObject1.VmdClip = vmd1Clip;
        const action1 = mixer.clipAction(vmd1Clip);
        action1.play();
      } else {
        console.warn(`Motion object with id "001" not found.`);
      }
    }, onProgress, onError);
    loader.loadAnimation(vmd2Path, stageObject, (vmd2Clip) => {
      vmd2Clip.name = "002";
      console.log(`Loaded VMD: ${vmd2Path}`);
      const motionObject2 = MotionObjects.find(obj => obj.id === "002");
      if (motionObject2) {
        motionObject2.VmdClip = vmd2Clip;
        const action2 = mixer.clipAction(vmd2Clip);
        action2.play();
      } else {
        console.warn(`Motion object with id "002" not found.`);
      }
    }, onProgress, onError);
    const clock = new THREE.Clock();
    const animate = () => {
      requestAnimationFrame(animate);
      const delta = clock.getDelta();
      mixer.update(delta);
      renderer.render(scene, camera);
    };
    animate();
  }, onProgress, onError);
} else {
  console.warn('No valid stage path found.');
}
//if (!Stage) {Stage = stage ? `stages${stage}` : '/sorclair/sorclair.pmx';}
if (!Pmx) {Pmx = `./pmx/pronama/AoiZaizen/AoiZaizen.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: "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: "lupin", pose: "lupin", VmdClip: null, AudioClip: true },
  { id: "lp-alightthatnevercomes", pose: "lp-alightthatnevercomes", VmdClip: null, AudioClip: true },
  { id: "pillarmen", pose: "pillarmen", VmdClip: null, AudioClip: true },
  { id: "halseywithoutme", pose: "halseywithoutme", VmdClip: null, AudioClip: true },
  { id: "riversinthedesert", pose: "riversinthedesert", 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();
  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(path) {
    return new Promise(resolve => {
      loader.load(path, object => {
        resolve(object);
      });
    });
}

  async function LoadAllModels() {
    mesh = await LoadPMX(Pmx);
    scene.add(mesh);

    if (Pmx2) {
      mesh2 = await LoadPMX(Pmx2);
  mesh2.position.x += 15; 
      scene.add(mesh2);
    }
    const stageMesh = await LoadPMX(StagePath);
    //scene.add(stageMesh); //else will add scene a second time !
  }

  async function LoadAllAssets() {
    await LoadAllModels();
    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");
    VmdControl("loop", true);
  }

  async 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;
        document.getElementById('readystate').innerHTML = "Checking motion vmd - " + id + ' (if stuck, click <a href="js/main.js" target="_blank">here</a>)';
        resolve(true);
      });
      if (mesh2) {
        loader.loadAnimation(path, mesh2, vmd => {
          vmd.name = id;
          MotionObjects[val].VmdClip = vmd;
          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);
      }
    });
  }

  await LoadAllAssets();
}

function VmdControl(id, loop) {
  const index = MotionObjects.findIndex(MotionObject => MotionObject.id == id);
  if (index === -1) {
    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 (mesh2) {
    helper.add(mesh2, {
      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) => {
  if (!Array.isArray(motionObjects)) {
    console.error("motionObjects is not an array.");
    return () => {};
  }
  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);
document.querySelectorAll(".pose-button").forEach(button => {
  button.addEventListener("click", (event) => {
    const poseId = event.target.dataset.poseId;
    PoseClickEvent(poseId);
  });
});

Could you post it as a possible bug issue in the official repo (together with the video / link to thread) ? MMDParser code does look a bit fragile, there’s a chance something is leaking scope there.

1 Like

Nah.
It was the original code of mine.
I since rewrote things called models twice.

It works now:
https://github.com/Ry3yr/SampleWebMMD

let scene, renderer, camera, mesh, mesh2;
let hasLoaded = false;
let mixer1, mixer2, clock, cameraAnimation;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
function getQueryStringValue(key) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(key);
}
const pmx = getQueryStringValue(‘pmx’);
const vmdpath = getQueryStringValue(‘vmd’) || “bts-bestofme”;
const pmx2 = getQueryStringValue(‘pmx2’) || “AoiZaizen/AoiZaizen.pmx”;
const cameraId = getQueryStringValue(‘camera’);
const stage = getQueryStringValue(‘stage’) || “/sorclair/sorclair.pmx”;
let Pmx;
let Pmx2;

if (pmx) {
Pmx = ./pmx/pronama/${pmx.trim()};
console.log(PMX: ${pmx.trim()});
} else {
console.log(“No PMX selected.”);
}
if (pmx2) {
Pmx2 = ./pmx/pronama/${pmx2.trim()};
console.log(PMX2: ${pmx2.trim()});
} else {
console.log(“No PMX2 selected.”);
}
let StagePath = stage ? ./stages/${stage.trim()} : ‘./stages/sorclair/sorclair.pmx’;
if (pmx) {
Pmx = ./pmx/pronama/${pmx.trim()};console.log(PMX: ${pmx.trim()});} else {console.log(“No PMX selected.”);}
if (pmx2) {Pmx2 = ./pmx/pronama/${pmx2.trim()};console.log(PMX2: ${pmx2.trim()});} else {console.log(“No PMX2 selected.”);}
if (StagePath) {StagePath = ./stages${stage.trim()};} else {StagePath = ‘./stages/sorclair/sorclair.pmx’;}
console.log(‘StagePath:’, StagePath);
if (StagePath) {
const loader = new THREE.MMDLoader();
const lastIndex = StagePath.lastIndexOf(“/”);
const basePath = StagePath.substring(0, lastIndex);
const vmd1Path = ${basePath}/001.vmd;
const vmd2Path = ${basePath}/002.vmd;
loader.load(StagePath, (stageObject) => {
var ambientLight = new THREE.AmbientLight(0xffffff, 1.0); //hardcoded
scene.add(ambientLight);
scene.add(stageObject);
const mixer = new THREE.AnimationMixer(stageObject);
loader.loadAnimation(vmd1Path, stageObject, (vmd1Clip) => {
vmd1Clip.name = “001”;
console.log(Loaded VMD: ${vmd1Path});
const motionObject1 = MotionObjects.find(obj => obj.id === “001”);
if (motionObject1) {
motionObject1.VmdClip = vmd1Clip;
const action1 = mixer.clipAction(vmd1Clip);
action1.play();
} else {
console.warn(Motion object with id "001" not found.);
}
}, onProgress, onError);
loader.loadAnimation(vmd2Path, stageObject, (vmd2Clip) => {
vmd2Clip.name = “002”;
console.log(Loaded VMD: ${vmd2Path});
const motionObject2 = MotionObjects.find(obj => obj.id === “002”);
if (motionObject2) {
motionObject2.VmdClip = vmd2Clip;
const action2 = mixer.clipAction(vmd2Clip);
action2.play();
} else {
console.warn(Motion object with id "002" not found.);
}
}, onProgress, onError);
const clock = new THREE.Clock();
const animate = () => {
requestAnimationFrame(animate);
const delta = clock.getDelta();
mixer.update(delta);
renderer.render(scene, camera);
};
animate();
}, onProgress, onError);
} else {console.warn(‘No valid stage path found.’);}
//if (!Stage) {Stage = stage ? stages${stage} : ‘/sorclair/sorclair.pmx’;}
if (!Pmx) {Pmx = ./pmx/pronama/AoiZaizen/AoiZaizen.pmx;}
console.log(‘StagePath:’, StagePath);
const MotionObjects = [
{ id: “001”, pose: “001”, VmdClip: null, AudioClip: false },
];
window.onload = () => {
Init();
LoadStage().then(() => {
LoadModels().then(() => {
});
});
};
function Init() {
document.getElementById(“moveLeftButton”).addEventListener(“click”, () => { camera.position.x -= 1; });
document.getElementById(“moveRightButton”).addEventListener(“click”, () => { camera.position.x += 1; });
document.getElementById(“moveUpButton”).addEventListener(“click”, () => { camera.position.y += 1; });
document.getElementById(“moveDownButton”).addEventListener(“click”, () => { camera.position.y -= 1; });
document.getElementById(“rotaterightButton”).addEventListener(“click”, () => { mesh.rotateY(Math.PI / 4); });
document.getElementById(“rotateleftButton”).addEventListener(“click”, () => { mesh.rotateY(-Math.PI / 4); });
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(100, windowWidth / windowHeight, 1, 1000);
camera.position.set(0, 19, 20);
clock = new THREE.Clock();
}
function LoadStage() {
return new Promise((resolve, reject) => {
const loader = new THREE.MMDLoader();
loader.load(StagePath, (stageObject) => {
resolve();
}, onProgress, reject);
});
}

let animate;
function startAnimation() {
document.getElementById(‘readystate’).innerHTML = ‘Camera(localstorage): ready - ’ + localStorage.getItem(‘vmd’) + ’ Reload’;
if (!animate) {
console.error(‘Animation function not initialized.’);
return;
}
animate(); // Start the animation loop
}
document.getElementById(‘play’).addEventListener(‘click’, async () => {
const urlParams = new URLSearchParams(window.location.search);
const vmdValue = urlParams.get(‘vmd’) || “bts-bestofme”;
if (!vmdValue) {
console.log(‘No vmd parameter found in the URL’);
return false;}
console.log(‘vmdValue from URL:’, vmdValue);
const audioPath = audio/${vmdValue}.mp3;
const audioListener = new THREE.AudioListener();
const audio = new THREE.Audio(audioListener);
const audioLoader = new THREE.AudioLoader();
try {
const audioBuffer = await new Promise((resolve, reject) => {
audioLoader.load(audioPath, resolve, onAudioLoadProgress, reject);
});
audio.setBuffer(audioBuffer);
audio.setLoop(true); // Set to true if audio should loop
audio.setVolume(1.0); // Adjust volume as needed
audio.play();
console.log(‘Audio loaded and playing:’, audioPath);
} catch (error) {
console.error(‘Error loading audio:’, error);
document.getElementById(‘readystate’).textContent = “Error loading Audio”;

    return false;
}
function onAudioLoadProgress(xhr) {
    if (xhr.lengthComputable) {
        const percentComplete = (xhr.loaded / xhr.total) * 100;
        console.log('Audio load progress:', percentComplete.toFixed(2) + '%');

document.getElementById(‘readystate’).textContent = 'Audio load progress: ’ + percentComplete.toFixed(2) + ‘%’;

}}

try {
startAnimation();
} catch (error) {
console.error(‘Error loading models:’, error);
}
});
async function LoadModels() {
const loader = new THREE.MMDLoader();
function LoadPMX(path) {
return new Promise(resolve => {
loader.load(path, (object) => {
resolve(object);
}, onProgress, onError);
});
}
async function LoadVMDAnimation(mesh, id) {
function getQueryStringParameter(name) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
const vmdId = getQueryStringParameter(‘vmd’) || ‘bts-bestofme’;
const vmdPath = ./vmd/${vmdId}.vmd;
localStorage.setItem(‘vmd’, vmdId);
return new Promise((resolve, reject) => {
loader.loadAnimation(vmdPath, mesh, (vmdClip) => {
vmdClip.name = vmdId;
resolve(vmdClip);
}, onProgress, reject);
});
}
async function LoadCameraAnimation(camera) {
let camid;
if (new URLSearchParams(window.location.search).has(‘camera’)) {
camid = new URLSearchParams(window.location.search).get(‘camera’);
} else if (new URLSearchParams(window.location.search).has(‘vmd’)) {
camid = new URLSearchParams(window.location.search).get(‘vmd’);
} else {
camid = localStorage.getItem(‘camid’);
if (!camid) {
camid = ‘bts-bestofme’;
}
}
const cameraVmdPath = “./camera/” + camid + “.vmd”;
try {
const vmdClip = await new Promise((resolve, reject) => {
loader.loadAnimation(cameraVmdPath, camera, (vmdClip) => {
vmdClip.name = camid; // Set the name to the loaded camid
resolve(vmdClip);
}, onProgress, reject);
});
return vmdClip;
} catch (error) {
console.error(‘Error loading camera animation:’, error);
throw error; // Re-throw the error to propagate it
}
}
async function LoadModel1() {
const mesh = await LoadPMX(Pmx);
scene.add(mesh);
const vmdClip = await LoadVMDAnimation(mesh, “001”);
const helper = new THREE.MMDAnimationHelper({ afterglow: 1.0 });
const mmd = { mesh: mesh, animation: vmdClip };
helper.add(mmd.mesh, {
animation: mmd.animation,
physics: true
});
return { mesh: mesh, helper: helper };
}
async function LoadModel2() {
if (Pmx2) {
const mesh2 = await LoadPMX(Pmx2);
mesh2.position.x += 15;
scene.add(mesh2);
const vmdClip = await LoadVMDAnimation(mesh2, “002”);
const helper = new THREE.MMDAnimationHelper({ afterglow: 1.0 });
const mmd = { mesh: mesh2, animation: vmdClip };
helper.add(mmd.mesh, {
animation: mmd.animation,
physics: true
});
return { mesh: mesh2, helper: helper };
}
}

const { mesh: mesh1, helper: helper1 } = await LoadModel1();
const { mesh: mesh2, helper: helper2 } = await LoadModel2();
const fov = 45; // Define the field of view
const aspect = window.innerWidth / window.innerHeight; // Define the aspect ratio
const near = 1; // Define the near clipping plane
const far = 1000; // Define the far clipping plane
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
const cameraVmdClip = await LoadCameraAnimation(camera);
const cameraHelper = new THREE.MMDAnimationHelper();
cameraHelper.add(camera, {
animation: cameraVmdClip
});
const clock = new THREE.Clock();

animate = () => {
requestAnimationFrame(animate);
const delta = clock.getDelta();
helper1.update(delta);
if (helper2) helper2.update(delta);
cameraHelper.update(delta); // Update camera animation
renderer.render(scene, camera);
};
}

function onProgress(xhr) {
if (xhr.lengthComputable) {
const percentComplete = xhr.loaded / xhr.total * 100;
console.log(Math.round(percentComplete) + ' downloaded'); document.getElementById('readystate').innerHTML = Math.round(percentComplete) + ' downloaded (if stuck, click here) ’ + 'Camera: ready - ’ + localStorage.getItem(‘camid’);
}
}
function onError(xhr) {
console.error(“Error loading resource:”, xhr);
document.getElementById(‘readystate’).textContent = "Error loading resource: " + xhr.statusText;

}

fullscreenButton.addEventListener('click', () => {
  if (!document.fullscreenElement) {
    //document.body.requestFullscreen();
    renderer.domElement.requestFullscreen();
  } else {
    if (document.exitFullscreen) {
      document.exitFullscreen();
    }
  }
});