e663915124
Debugging message logs + wireframe mode Textures with spaces in their filename not supported
421 lines
15 KiB
HTML
421 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ja">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||
<title>PMX Loader from ZIP</title>
|
||
<style>
|
||
body { margin: 0; overflow: hidden; background: #000; font-family: monospace; }
|
||
#loading { position: absolute; top: 10px; left: 10px; color: #0f0; background: #000; padding: 4px 10px; font-size: 14px; z-index: 10; }
|
||
#log { position: absolute; bottom: 0; left: 0; width: 100%; height: 150px; overflow-y: auto; background: rgba(0,0,0,.8); color: #0f0; font-size: 12px; padding: 5px; box-sizing: border-box; white-space: pre-wrap; }
|
||
#controls { position: absolute; top: 10px; right: 10px; z-index: 10; display: flex; flex-direction: column; }
|
||
.control-btn { font-size: 20px; padding: 10px; margin: 5px; background: #333; color: #fff; border: none; border-radius: 6px; cursor: pointer; user-select: none; }
|
||
.error { color: #f00; }
|
||
.warning { color: #ff0; }
|
||
.info { color: #0f0; }
|
||
#wireframeBtn { font-size: 16px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="loading">Initializing PMX loader...</div>
|
||
<div id="log"></div>
|
||
<div id="controls">
|
||
<button class="control-btn" id="zoomIn">+</button>
|
||
<button class="control-btn" id="zoomOut">−</button>
|
||
<button class="control-btn" id="wireframeBtn">Wireframe OFF</button>
|
||
</div>
|
||
|
||
<script src="./libs/three.js"></script>
|
||
<script src="./libs/mmdparser-obsolete.min.js"></script>
|
||
<script src="./libs/ammo.min.js"></script>
|
||
<script src="./libs/TGALoader.js"></script>
|
||
<script src="./libs/MMDLoader.js"></script>
|
||
<script src="./libs/MMDAnimationHelper.js"></script>
|
||
<script src="./libs/CCDIKSolver.js"></script>
|
||
<script src="./libs/MMDPhysics.js"></script>
|
||
|
||
<script>
|
||
// Override console methods to capture all output
|
||
const originalConsole = {
|
||
log: console.log,
|
||
warn: console.warn,
|
||
error: console.error
|
||
};
|
||
|
||
function logToDiv(type, ...args) {
|
||
const msg = args.join(' ');
|
||
const logEl = document.getElementById('log');
|
||
const className = type === 'error' ? 'error' :
|
||
type === 'warn' ? 'warning' : 'info';
|
||
|
||
logEl.innerHTML += `<span class="${className}">${msg}</span>\n`;
|
||
logEl.scrollTop = logEl.scrollHeight;
|
||
}
|
||
|
||
function log(...args) {
|
||
const msg = args.join(' ');
|
||
originalConsole.log(msg);
|
||
logToDiv('info', msg);
|
||
}
|
||
|
||
function warn(...args) {
|
||
const msg = args.join(' ');
|
||
originalConsole.warn(msg);
|
||
logToDiv('warn', msg);
|
||
}
|
||
|
||
function error(...args) {
|
||
const msg = args.join(' ');
|
||
originalConsole.error(msg);
|
||
logToDiv('error', msg);
|
||
}
|
||
|
||
// Override console methods
|
||
console.log = log;
|
||
console.warn = warn;
|
||
console.error = error;
|
||
|
||
// Capture unhandled errors
|
||
window.addEventListener('error', function(e) {
|
||
error(`Unhandled error: ${e.message} (${e.filename}:${e.lineno}:${e.colno})`);
|
||
if (e.error && e.error.stack) {
|
||
error(e.error.stack);
|
||
}
|
||
});
|
||
|
||
// Capture unhandled promise rejections
|
||
window.addEventListener('unhandledrejection', function(e) {
|
||
error('Unhandled promise rejection:', e.reason);
|
||
if (e.reason && e.reason.stack) {
|
||
error(e.reason.stack);
|
||
}
|
||
});
|
||
|
||
// Track texture loading errors
|
||
let missingTextures = 0;
|
||
|
||
// Patch THREE.TextureLoader to catch texture loading errors
|
||
const originalLoad = THREE.TextureLoader.prototype.load;
|
||
THREE.TextureLoader.prototype.load = function(url, onLoad, onProgress, onError) {
|
||
const patchedOnError = (err) => {
|
||
missingTextures++;
|
||
error(`Failed to load texture: ${url}`);
|
||
if (onError) onError(err);
|
||
};
|
||
return originalLoad.call(this, url, onLoad, onProgress, patchedOnError);
|
||
};
|
||
|
||
// Patch ImageLoader to catch image loading errors
|
||
const originalImageLoad = THREE.ImageLoader.prototype.load;
|
||
THREE.ImageLoader.prototype.load = function(url, onLoad, onProgress, onError) {
|
||
const patchedOnError = (err) => {
|
||
missingTextures++;
|
||
error(`Failed to load image: ${url}`);
|
||
if (onError) onError(err);
|
||
};
|
||
return originalImageLoad.call(this, url, onLoad, onProgress, patchedOnError);
|
||
};
|
||
|
||
const currentDir = new URL('.', window.location.href).href;
|
||
const swPath = new URL('zipfsmmd-sw.js', currentDir).href;
|
||
const FALLBACK_ZIP = new URL('models/AoiZaizen.zip', currentDir).href;
|
||
|
||
function getZipUrl(){
|
||
const params = new URLSearchParams(window.location.search);
|
||
const zip = params.get('pmx');
|
||
if(zip) return new URL(zip, window.location.href).href;
|
||
log('No ?pmx= query parameter found; falling back to default ZIP');
|
||
return FALLBACK_ZIP;
|
||
}
|
||
|
||
function getPmxFileName(){
|
||
const params = new URLSearchParams(window.location.search);
|
||
let modelFile = params.get('model');
|
||
if(modelFile){
|
||
if(!modelFile.startsWith('/')) modelFile = '/' + modelFile;
|
||
return modelFile;
|
||
}
|
||
const zipUrl = getZipUrl();
|
||
const zipBase = zipUrl.split('/').pop().replace(/\.zip$/i, '');
|
||
return '/' + zipBase + '.pmx';
|
||
}
|
||
|
||
const zipPath = getZipUrl();
|
||
const pmxFileName = getPmxFileName();
|
||
|
||
const vmdName = 'bts-bestofme';
|
||
|
||
if('serviceWorker' in navigator){
|
||
navigator.serviceWorker.register(swPath).then(reg=>{
|
||
log(`Service Worker registered at: ${swPath}`);
|
||
if(!navigator.serviceWorker.controller) setTimeout(()=>location.reload(),500);
|
||
else{
|
||
navigator.serviceWorker.controller.postMessage({type:'SET_ZIP_PATH',zipPath});
|
||
initLoader();
|
||
}
|
||
}).catch(err=>{
|
||
error('SW registration failed:', err);
|
||
initLoader();
|
||
});
|
||
|
||
navigator.serviceWorker.addEventListener('message',event=>{
|
||
if(event.data && event.data.type==='SW_LOG') log('[SW]', event.data.text);
|
||
});
|
||
}else{
|
||
warn('Service Workers not supported');
|
||
initLoader();
|
||
}
|
||
|
||
async function initLoader(){
|
||
log('Initializing PMX loader...');
|
||
log(`ZIP path: ${zipPath}`);
|
||
log(`PMX model path inside ZIP: ${pmxFileName}`);
|
||
log(`VMD name: ${vmdName}`);
|
||
|
||
let scene, renderer, camera, mesh, helper;
|
||
let ready = false;
|
||
const clock = new THREE.Clock();
|
||
let theta = Math.PI / 4, phi = Math.PI / 4, radius = 50;
|
||
let isMouseDown = false, previousMousePosition = {x: 0, y: 0};
|
||
|
||
function initScene(){
|
||
scene = new THREE.Scene();
|
||
scene.add(new THREE.AmbientLight(0xeeeeee));
|
||
renderer = new THREE.WebGLRenderer({alpha:true,antialias:true});
|
||
renderer.setPixelRatio(window.devicePixelRatio);
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
document.body.appendChild(renderer.domElement);
|
||
|
||
camera = new THREE.PerspectiveCamera(40, window.innerWidth/window.innerHeight, 1, 1000);
|
||
updateCameraPosition();
|
||
|
||
document.addEventListener('mousedown', onMouseDown);
|
||
document.addEventListener('mousemove', onMouseMove);
|
||
document.addEventListener('mouseup', onMouseUp);
|
||
window.addEventListener('resize', onWindowResize);
|
||
|
||
// Touch controls
|
||
document.addEventListener('touchstart', onTouchStart);
|
||
document.addEventListener('touchmove', onTouchMove);
|
||
document.addEventListener('touchend', onTouchEnd);
|
||
|
||
document.getElementById('zoomIn').addEventListener('click', ()=>zoomCamera(-5));
|
||
document.getElementById('zoomOut').addEventListener('click', ()=>zoomCamera(5));
|
||
|
||
// Wireframe toggle button
|
||
document.getElementById('wireframeBtn').addEventListener('click', toggleWireframe);
|
||
}
|
||
|
||
function toggleWireframe() {
|
||
if (!mesh) return;
|
||
|
||
let isWireframe = false;
|
||
mesh.traverse(function(child) {
|
||
if (child.isMesh) {
|
||
if (Array.isArray(child.material)) {
|
||
child.material.forEach(mat => {
|
||
mat.wireframe = !mat.wireframe;
|
||
isWireframe = mat.wireframe || isWireframe;
|
||
|
||
// If textures are missing, set wireframe color to light green
|
||
if (mat.wireframe && missingTextures > 0) {
|
||
mat.color.set(0x90EE90); // Light green
|
||
mat.emissive.set(0x90EE90);
|
||
mat.needsUpdate = true;
|
||
} else if (!mat.wireframe) {
|
||
// Reset to default colors when wireframe is off
|
||
mat.color.set(0xFFFFFF);
|
||
mat.emissive.set(0x000000);
|
||
mat.needsUpdate = true;
|
||
}
|
||
});
|
||
} else {
|
||
child.material.wireframe = !child.material.wireframe;
|
||
isWireframe = child.material.wireframe || isWireframe;
|
||
|
||
// If textures are missing, set wireframe color to light green
|
||
if (child.material.wireframe && missingTextures > 0) {
|
||
child.material.color.set(0x90EE90); // Light green
|
||
child.material.emissive.set(0x90EE90);
|
||
child.material.needsUpdate = true;
|
||
} else if (!child.material.wireframe) {
|
||
// Reset to default colors when wireframe is off
|
||
child.material.color.set(0xFFFFFF);
|
||
child.material.emissive.set(0x000000);
|
||
child.material.needsUpdate = true;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
document.getElementById('wireframeBtn').textContent = isWireframe ? 'Wireframe ON' : 'Wireframe OFF';
|
||
log(`Wireframe mode ${isWireframe ? 'enabled' : 'disabled'}`);
|
||
|
||
if (isWireframe && missingTextures > 0) {
|
||
log(`Using light green wireframe (${missingTextures} missing textures)`);
|
||
}
|
||
}
|
||
|
||
function onMouseDown(e){e.preventDefault(); isMouseDown=true; previousMousePosition={x:e.clientX,y:e.clientY};}
|
||
function onMouseMove(e){
|
||
if(!isMouseDown) return;
|
||
e.preventDefault();
|
||
const dx = e.clientX - previousMousePosition.x;
|
||
const dy = e.clientY - previousMousePosition.y;
|
||
theta -= dx * 0.005;
|
||
phi -= dy * 0.005;
|
||
phi = Math.min(Math.max(phi,0.01), Math.PI-0.01);
|
||
updateCameraPosition();
|
||
previousMousePosition = {x:e.clientX,y:e.clientY};
|
||
}
|
||
function onMouseUp(e){e.preventDefault(); isMouseDown=false;}
|
||
|
||
// Touch controls
|
||
let touchStartX = 0, touchStartY = 0;
|
||
function onTouchStart(e) {
|
||
e.preventDefault();
|
||
touchStartX = e.touches[0].clientX;
|
||
touchStartY = e.touches[0].clientY;
|
||
}
|
||
function onTouchMove(e) {
|
||
e.preventDefault();
|
||
const dx = e.touches[0].clientX - touchStartX;
|
||
const dy = e.touches[0].clientY - touchStartY;
|
||
theta -= dx * 0.005;
|
||
phi -= dy * 0.005;
|
||
phi = Math.min(Math.max(phi, 0.01), Math.PI - 0.01);
|
||
updateCameraPosition();
|
||
touchStartX = e.touches[0].clientX;
|
||
touchStartY = e.touches[0].clientY;
|
||
}
|
||
function onTouchEnd(e) { }
|
||
|
||
function zoomCamera(delta){
|
||
radius += delta;
|
||
radius = Math.min(Math.max(radius,10),200);
|
||
updateCameraPosition();
|
||
}
|
||
|
||
function updateCameraPosition(){
|
||
camera.position.x = radius * Math.sin(phi) * Math.cos(theta);
|
||
camera.position.y = radius * Math.cos(phi);
|
||
camera.position.z = radius * Math.sin(phi) * Math.sin(theta);
|
||
camera.lookAt(scene.position);
|
||
}
|
||
|
||
function onWindowResize(){
|
||
camera.aspect = window.innerWidth / window.innerHeight;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
}
|
||
|
||
async function loadModel(){
|
||
document.getElementById('loading').textContent = 'Loading model from ZIP...';
|
||
return new Promise((resolve, reject) => {
|
||
const loader = new THREE.MMDLoader();
|
||
|
||
// Patch MMDLoader's texture loading
|
||
const originalLoadTexture = loader._loadTexture;
|
||
loader._loadTexture = function(path, modelPath, onLoad, onProgress, onError) {
|
||
const patchedOnError = (err) => {
|
||
missingTextures++;
|
||
error(`Failed to load MMD texture: ${path}`);
|
||
if (onError) onError(err);
|
||
};
|
||
return originalLoadTexture.call(this, path, modelPath, onLoad, onProgress, patchedOnError);
|
||
};
|
||
|
||
loader.load(pmxFileName,
|
||
object => {
|
||
mesh = object;
|
||
mesh.position.y = -10;
|
||
scene.add(mesh);
|
||
document.getElementById('loading').textContent = 'Loading animation...';
|
||
log('PMX model loaded successfully from ZIP');
|
||
if (missingTextures > 0) {
|
||
warn(`${missingTextures} textures failed to load`);
|
||
}
|
||
resolve();
|
||
},
|
||
xhr => {
|
||
if(xhr.lengthComputable){
|
||
const percent = (xhr.loaded / xhr.total) * 100;
|
||
document.getElementById('loading').textContent = `Loading model... ${percent.toFixed(1)}%`;
|
||
}
|
||
},
|
||
err => {
|
||
error('Model load error:', err);
|
||
document.getElementById('loading').textContent = 'Model load failed!';
|
||
reject(err);
|
||
});
|
||
});
|
||
}
|
||
|
||
async function loadAnimation(){
|
||
return new Promise((resolve, reject) => {
|
||
const loader = new THREE.MMDLoader();
|
||
const vmdPath = new URL(`vmd/${vmdName}.vmd`, currentDir).href;
|
||
log(`Loading VMD from: ${vmdPath}`);
|
||
loader.loadAnimation(vmdPath, mesh,
|
||
vmdClip => {
|
||
vmdClip.name = vmdName;
|
||
setupAnimation(vmdClip);
|
||
document.getElementById('loading').style.display = 'none';
|
||
log('Animation loaded successfully');
|
||
resolve();
|
||
},
|
||
xhr => {
|
||
if(xhr.lengthComputable){
|
||
const percent = (xhr.loaded / xhr.total) * 100;
|
||
document.getElementById('loading').textContent = `Loading animation... ${percent.toFixed(1)}%`;
|
||
}
|
||
},
|
||
err => {
|
||
error('Animation load error:', err);
|
||
document.getElementById('loading').textContent = 'Animation load failed!';
|
||
reject(err);
|
||
});
|
||
});
|
||
}
|
||
|
||
function setupAnimation(vmdClip){
|
||
try {
|
||
ready = false;
|
||
helper = new THREE.MMDAnimationHelper({afterglow: 2, resetPhysicsOnLoop: true});
|
||
helper.add(mesh, { animation: vmdClip, physics: true });
|
||
const mixer = helper.objects.get(mesh).mixer;
|
||
mixer.addEventListener('finished', () => {
|
||
log('Animation finished. Restarting...');
|
||
setupAnimation(vmdClip);
|
||
});
|
||
ready = true;
|
||
} catch (e) {
|
||
error('Error setting up animation:', e);
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
function animate(){
|
||
try {
|
||
requestAnimationFrame(animate);
|
||
if(ready && helper) helper.update(clock.getDelta());
|
||
renderer.render(scene, camera);
|
||
} catch (e) {
|
||
error('Animation loop error:', e);
|
||
}
|
||
}
|
||
|
||
initScene();
|
||
|
||
try {
|
||
await loadModel();
|
||
await loadAnimation();
|
||
animate();
|
||
log('PMX viewer ready');
|
||
} catch(e) {
|
||
error('Initialization failed:', e);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |