Loading models...

PMX + VPD Viewer

Pinch to zoom | Drag to rotate/move

Mode: Model Control
No bone selected
100%

Stored Poses

Head & Neck

Shoulders

Arms

Elbows

Hands

Legs

Knees

Feet

Spine

Controls Guide

Mobile Touch Controls

Pinch - Zoom in/out

One Finger Drag - Rotate model/camera

Two Finger Drag - Pan camera

Tap Orb - Select bone

Drag Orb - Move/rotate bone

Desktop Controls

Mouse Drag - Rotate

Right Click + Drag - Pan

Scroll Wheel - Zoom

Bone Editing

Store Pose - Save current bone positions

Load Pose - Restore saved pose

Generate URL - Save full scene state

loat(parts[1]), parseFloat(parts[2])); model2.rotation.set(parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5])); } } } function resolveAssetPath(assetPath, defaultBase) { if (!assetPath) return null; assetPath = assetPath.trim(); if (/^(https?:|file:|\/)/i.test(assetPath)) return assetPath; if (/^(\.\/|\.\.\/)/.test(assetPath)) return assetPath; if (/^vpd\//i.test(assetPath)) return "./" + assetPath; return (defaultBase || "./pmx/pronama/") + assetPath; } function loadModels() { if (pmxPath1) { const full1 = resolveAssetPath(pmxPath1, "./pmx/pronama/"); loader.load(full1, object => { model1 = object; model1.position.set(-5, 0, 0); function enableShadows(obj) { if (obj.isMesh) { obj.castShadow = true; obj.receiveShadow = true; } obj.children.forEach(enableShadows); } enableShadows(model1); scene.add(model1); if (vpdPath1) loadAndApplyVPD(model1, resolveAssetPath(vpdPath1, "./")); applyModelStates(); updateModelInfo(); checkLoadingComplete(); }, null, err => console.error("Error loading PMX1", err)); } if (pmxPath2) { const full2 = resolveAssetPath(pmxPath2, "./pmx/pronama/"); loader.load(full2, object => { model2 = object; model2.position.set(5, 0, 0); function enableShadows(obj) { if (obj.isMesh) { obj.castShadow = true; obj.receiveShadow = true; } obj.children.forEach(enableShadows); } enableShadows(model2); scene.add(model2); if (vpdPath2) loadAndApplyVPD(model2, resolveAssetPath(vpdPath2, "./")); applyModelStates(); updateModelInfo(); checkLoadingComplete(); }, null, err => console.error("Error loading PMX2", err)); } if (!pmxPath1 && !pmxPath2) document.getElementById("loading").textContent = "No PMX models specified."; setTimeout(loadStateFromURL, 100); } function loadAndApplyVPD(model, vpdPath) { loader.loadVPD(vpdPath, false, function(vpd) { requestAnimationFrame(() => { try { helper.pose(model, vpd); } catch (error) { console.error("Error applying pose:", error); } }); }, null, function(error) { console.error("Error loading VPD:", error); }); } function updateModelInfo() { let info = ""; if (model1) info += "Model 1 loaded" + (vpdPath1 ? " + Pose 1" : "") + "
"; if (model2) info += "Model 2 loaded" + (vpdPath2 ? " + Pose 2" : "") + "
"; document.getElementById("modelInfo").innerHTML = info; } function checkLoadingComplete() { const total = [pmxPath1, pmxPath2].filter(Boolean).length; const loaded = [model1, model2].filter(Boolean).length; if (loaded === total) { document.getElementById("loading").style.display = "none"; setTimeout(() => { applyStoredPosesOnLoad(); }, 1500); } } function applyStoredPosesOnLoad() { const storedPose1 = localStorage.getItem('stor1'); if (storedPose1 && model1) { try { const pose = JSON.parse(storedPose1); applyPoseToModelFromStorage(model1, pose); } catch (error) { console.error('Error applying stored pose 1:', error); } } const storedPose2 = localStorage.getItem('stor2'); if (storedPose2 && model2) { try { const pose = JSON.parse(storedPose2); applyPoseToModelFromStorage(model2, pose); } catch (error) { console.error('Error applying stored pose 2:', error); } } } function applyPoseToModelFromStorage(model, pose) { if (!model || !model.skeleton || !pose) return; model.updateMatrixWorld(true); Object.keys(pose).forEach(japaneseName => { const bone = model.skeleton.bones.find(b => b.name === japaneseName); if (!bone || !pose[japaneseName]) return; const relQuat = new THREE.Quaternion(pose[japaneseName].quaternion.x, pose[japaneseName].quaternion.y, pose[japaneseName].quaternion.z, pose[japaneseName].quaternion.w); bone.quaternion.copy(relQuat).normalize(); }); model.skeleton.update(); model.updateMatrixWorld(true); } function resetView() { cameraTarget.set(0, 0, 0); cameraDistance = 25; cameraPhi = Math.PI / 6; cameraTheta = 0; updateCameraPosition(); if (model1) { model1.position.set(-5, 0, 0); model1.rotation.set(0, 0, 0); } if (model2) { model2.position.set(5, 0, 0); model2.rotation.set(0, 0, 0); } } function toggleWireframe() { [model1, model2].forEach(model => { if (!model) return; model.traverse(c => { if (c.isMesh) { if (Array.isArray(c.material)) c.material.forEach(m => m.wireframe = !m.wireframe); else c.material.wireframe = !c.material.wireframe; } }); }); } function toggleControlMode() { if (controlMode === 'bone') return; controlMode = controlMode === 'model' ? 'camera' : 'model'; updateModeUI(); } function toggleBoneMode() { if (controlMode === 'bone') { controlMode = 'model'; document.getElementById('bone-controls').style.display = 'none'; document.getElementById('selected-bone-info').style.display = 'none'; document.getElementById('bone-transform-controls').style.display = 'none'; document.getElementById('axis-controls').style.display = 'none'; if (orbGroup) scene.remove(orbGroup); boneOrbs = []; selectedOrb = null; } else { controlMode = 'bone'; document.getElementById('bone-controls').style.display = 'flex'; document.getElementById('bone-transform-controls').style.display = 'flex'; document.getElementById('selected-bone-info').style.display = 'block'; const targetModels = getTargetModels(); selectedModel = targetModels[0] || null; activeBoneModel = selectedModel; if(activeBoneModel) { if(!activeBoneModel.skeleton.bones[0].userData.bindQuaternion) { activeBoneModel.skeleton.bones.forEach(b => { b.userData.bindQuaternion = b.quaternion.clone(); }); } rebuildOrbs(); } setBoneTransformMode('move'); } updateModeUI(); } function updateModeUI() { const modeBtn = document.getElementById('toggleMode'); const boneBtn = document.getElementById('toggleBones'); const modeIndicator = document.getElementById('mode-indicator'); if (controlMode === 'camera') { modeBtn.textContent = 'Model Mode'; modeBtn.classList.add('active'); boneBtn.textContent = 'Bone Mode'; boneBtn.classList.remove('active'); modeIndicator.textContent = 'Mode: Camera Control'; } else if (controlMode === 'model') { modeBtn.textContent = 'Camera Mode'; modeBtn.classList.remove('active'); boneBtn.textContent = 'Bone Mode'; boneBtn.classList.remove('active'); modeIndicator.textContent = 'Mode: Model Control'; } else if (controlMode === 'bone') { modeBtn.textContent = 'Camera Mode'; modeBtn.classList.remove('active'); boneBtn.textContent = 'Exit Bones'; boneBtn.classList.add('active'); modeIndicator.textContent = 'Mode: Bone Editing'; } } function selectModel(model) { currentModel = model; document.querySelectorAll('.model-btn').forEach(btn => { btn.classList.remove('active'); if (btn.getAttribute('data-model') === model) btn.classList.add('active'); }); if (controlMode === 'bone') { activeBoneModel = getTargetModels()[0] || null; selectedModel = activeBoneModel; if(activeBoneModel) rebuildOrbs(); } } function setupBoneHelpers() { boneHelpers.forEach(helper => scene.remove(helper)); boneHelpers = []; const targetModels = getTargetModels(); targetModels.forEach(model => { if (model && model.skeleton) { model.skeleton.bones.forEach(bone => { const geometry = new THREE.SphereGeometry(0.15, 12, 12); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.8 }); const helper = new THREE.Mesh(geometry, material); const worldPos = new THREE.Vector3(); bone.getWorldPosition(worldPos); helper.position.copy(worldPos); helper.userData = { bone: bone, model: model }; boneHelpers.push(helper); scene.add(helper); }); } }); } function findBoneInModel(model, boneName) { if (!model || !model.skeleton) return null; return model.skeleton.bones.find(bone => bone.name.toLowerCase().includes(boneName.toLowerCase())); } function selectBoneByType(boneType) { if (!selectedModel) return; const possibleNames = boneMappings[boneType]; if (!possibleNames) return; for (const name of possibleNames) { const bone = findBoneInModel(selectedModel, name); if (bone) { selectedBone = bone; updateBoneSelection(); document.querySelectorAll('.bone-btn').forEach(btn => btn.classList.remove('active')); document.querySelector(`.bone-btn[data-bone="${boneType}"]`).classList.add('active'); return; } } } function updateBoneSelection() { updateSelectedBoneInfo(); if (selectedBone && selectedModel) { boneHelpers.forEach(helper => { if (helper.userData.bone === selectedBone) { helper.material.color.set(0xff0000); helper.scale.set(1.5, 1.5, 1.5); } else { helper.material.color.set(0x00ff00); helper.scale.set(1, 1, 1); } }); } else { boneHelpers.forEach(helper => { helper.material.color.set(0x00ff00); helper.scale.set(1, 1, 1); }); } } function updateSelectedBoneInfo() { const infoElement = document.getElementById('selected-bone-info'); if (selectedBone && selectedModel) { infoElement.textContent = `Selected: ${selectedBone.name} (${boneTransformMode} mode)`; infoElement.style.display = 'block'; } else { infoElement.textContent = 'No bone selected'; infoElement.style.display = 'block'; } } function setBoneTransformMode(mode) { boneTransformMode = mode; document.getElementById('move-bone').classList.toggle('active', mode === 'move'); document.getElementById('rotate-bone').classList.toggle('active', mode === 'rotate'); updateSelectedBoneInfo(); } function updateBoneButtons() { if (!selectedBone) return; const boneName = selectedBone.name.toLowerCase(); document.querySelectorAll('.bone-btn').forEach(btn => { btn.classList.remove('active'); const boneType = btn.getAttribute('data-bone'); const possibleNames = boneMappings[boneType]; if (possibleNames && possibleNames.some(name => boneName.includes(name.toLowerCase()))) btn.classList.add('active'); }); } function reloadStoredPose(storageKey, model) { if (!model) return; const storedPose = localStorage.getItem(storageKey); if (storedPose) { try { const pose = JSON.parse(storedPose); applyPoseToModelFromStorage(model, pose); showPoseReloadFeedback(storageKey); } catch (error) { console.error(`Error applying ${storageKey}:`, error); } } } function reloadBothStoredPoses() { if (model1) reloadStoredPose('stor1', model1); if (model2) reloadStoredPose('stor2', model2); showPoseReloadFeedback('both'); } function showPoseReloadFeedback(poseType) { const feedback = document.createElement('div'); feedback.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.8);color:#8a8aff;padding:10px 20px;border-radius:10px;z-index:100;font-size:16px;font-weight:bold'; feedback.textContent = poseType === 'both' ? 'Both Poses Reloaded!' : `Pose ${poseType.slice(-1)} Reloaded!`; document.body.appendChild(feedback); setTimeout(() => document.body.removeChild(feedback), 1000); } function moveModelsUp() { const moveSpeed = 0.5; getTargetModels().forEach(model => { if (model) model.position.y += moveSpeed; }); } function moveModelsDown() { const moveSpeed = 0.5; getTargetModels().forEach(model => { if (model) model.position.y -= moveSpeed; }); } function setupMouseControls() { const container = document.getElementById('container'); let isMouseDown = false; let isRightClick = false; let isMiddleClick = false; let lastMouseX = 0, lastMouseY = 0; container.addEventListener('mousedown', function(e) { isMouseDown = true; isRightClick = e.button === 2; isMiddleClick = e.button === 1; lastMouseX = e.clientX; lastMouseY = e.clientY; const mouse = new THREE.Vector2(); mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); if (controlMode === 'bone') { const hits = raycaster.intersectObjects(boneOrbs); if (hits.length) { isDraggingOrb = true; currentOrb = hits[0].object; selectOrb(currentOrb); e.preventDefault(); return; } if (handleBoneDragStart(e, false)) return; const helperHits = raycaster.intersectObjects(boneHelpers); if (helperHits.length > 0 && e.button === 0) { selectedBone = helperHits[0].object.userData.bone; selectedModel = helperHits[0].object.userData.model; updateBoneSelection(); updateBoneButtons(); } } const targetModels = getTargetModels(); let hitModel = false; for (const model of targetModels) { if (model && raycaster.intersectObject(model, true).length > 0) { hitModel = true; break; } } if (hitModel && controlMode === 'model' && e.button === 0) { isMovingModel = true; moveStartX = e.clientX; moveStartY = e.clientY; } else { isRotating = true; rotateStartX = e.clientX; rotateStartY = e.clientY; } e.preventDefault(); }); container.addEventListener('mousemove', function(e) { if (controlMode === 'bone' && isDraggingOrb && selectedOrb) { const deltaX = e.movementX * 0.012; const deltaY = e.movementY * 0.012; const bone = selectedOrb.userData.bone; if (boneTransformMode === 'move') { bone.position.x += deltaX; bone.position.y -= deltaY; } else { bone.rotation.y += deltaX; bone.rotation.x += deltaY; } if (activeBoneModel) activeBoneModel.skeleton.update(); updateOrbPositions(); return; } if (controlMode === 'bone' && isDraggingBone) { handleBoneDrag(e, false); return; } if (!isMouseDown) return; const deltaX = e.clientX - lastMouseX; const deltaY = e.clientY - lastMouseY; if (isMovingModel && controlMode === 'model') { getTargetModels().forEach(model => { if (model) { model.position.x += deltaX * 0.02; model.position.z -= deltaY * 0.02; } }); } else if (isRotating) { if (controlMode === 'model') { const targetModels = getTargetModels(); if (isRightClick) targetModels.forEach(model => { if (model) { model.position.x += deltaX * 0.02; model.position.y -= deltaY * 0.02; } }); else targetModels.forEach(model => { if (model) { model.rotation.y += deltaX * 0.01; model.rotation.x += deltaY * 0.01; } }); } else if (controlMode === 'camera') { if (isRightClick || isMiddleClick) { const panSpeed = 0.01; const panVector = new THREE.Vector3(-deltaX, deltaY, 0).multiplyScalar(panSpeed); panVector.applyQuaternion(camera.quaternion); cameraTarget.add(panVector); updateCameraPosition(); } else { cameraTheta -= deltaX * 0.01; cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi + deltaY * 0.01)); updateCameraPosition(); } } } lastMouseX = e.clientX; lastMouseY = e.clientY; }); container.addEventListener('mouseup', function() { isMouseDown = false; isMovingModel = false; isRotating = false; if (controlMode === 'bone') { handleBoneDragEnd(); isDraggingOrb = false; currentOrb = null; } }); container.addEventListener('click', function(e) { if (controlMode === 'bone') { const mouse = new THREE.Vector2(); mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const hits = raycaster.intersectObjects(boneOrbs); if (hits.length) showAxisControls(hits[0].object); else document.getElementById('axis-controls').style.display = 'none'; } }); container.addEventListener('wheel', function(e) { if (controlMode !== 'bone') { cameraDistance = Math.max(5, Math.min(100, cameraDistance + e.deltaY * 0.03)); updateCameraPosition(); } e.preventDefault(); }); container.addEventListener('contextmenu', function(e) { e.preventDefault(); }); } function setupTouchControls() { const container = document.getElementById('container'); let touchStateLocal = { isTwoFinger: false, initialDistance: 0, initialPan: { x: 0, y: 0 } }; container.addEventListener('touchstart', function(e) { if (controlMode === 'bone' && e.touches.length === 1) { const touch = e.touches[0]; const mouse = new THREE.Vector2(); mouse.x = (touch.clientX / window.innerWidth) * 2 - 1; mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const hits = raycaster.intersectObjects(boneOrbs); if (hits.length) { isDraggingOrb = true; currentOrb = hits[0].object; selectOrb(currentOrb); e.preventDefault(); return; } } if (e.touches.length === 1) { const touch = e.touches[0]; const mouse = new THREE.Vector2(); mouse.x = (touch.clientX / window.innerWidth) * 2 - 1; mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const targetModels = getTargetModels(); let hitModel = false; for (const model of targetModels) { if (model && raycaster.intersectObject(model, true).length > 0) { hitModel = true; break; } } if (hitModel && controlMode === 'model') { isMovingModel = true; moveStartX = touch.clientX; moveStartY = touch.clientY; } else { isRotating = true; rotateStartX = touch.clientX; rotateStartY = touch.clientY; } } else if (e.touches.length === 2) { touchStateLocal.isTwoFinger = true; const touch1 = e.touches[0], touch2 = e.touches[1]; touchStateLocal.initialDistance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY); touchStateLocal.initialPan.x = (touch1.clientX + touch2.clientX) / 2; touchStateLocal.initialPan.y = (touch1.clientY + touch2.clientY) / 2; panStartX = touchStateLocal.initialPan.x; panStartY = touchStateLocal.initialPan.y; } e.preventDefault(); }); container.addEventListener('touchmove', function(e) { if (controlMode === 'bone' && isDraggingOrb && selectedOrb && e.touches.length === 1) { const touch = e.touches[0]; const dx = (touch.clientX - (window.lastTouchX || touch.clientX)) * 0.012; const dy = (touch.clientY - (window.lastTouchY || touch.clientY)) * 0.012; const bone = selectedOrb.userData.bone; if (boneTransformMode === 'move') { bone.position.x += dx; bone.position.y -= dy; } else { bone.rotation.y += dx; bone.rotation.x += dy; } if (activeBoneModel) activeBoneModel.skeleton.update(); updateOrbPositions(); window.lastTouchX = touch.clientX; window.lastTouchY = touch.clientY; e.preventDefault(); return; } if (e.touches.length === 1 && (isMovingModel || isRotating)) { const touch = e.touches[0]; const deltaX = touch.clientX - (window.lastTouchX || touch.clientX); const deltaY = touch.clientY - (window.lastTouchY || touch.clientY); if (isMovingModel) getTargetModels().forEach(model => { if (model) { model.position.x += deltaX * 0.02; model.position.z -= deltaY * 0.02; } }); else if (isRotating) { if (controlMode === 'model') getTargetModels().forEach(model => { if (model) { model.rotation.y += deltaX * 0.01; model.rotation.x += deltaY * 0.01; } }); else if (controlMode === 'camera') { cameraTheta -= deltaX * 0.01; cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi + deltaY * 0.01)); updateCameraPosition(); } } window.lastTouchX = touch.clientX; window.lastTouchY = touch.clientY; } else if (e.touches.length === 2 && touchStateLocal.isTwoFinger) { const touch1 = e.touches[0], touch2 = e.touches[1]; const currentDistance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY); const zoomDelta = (touchStateLocal.initialDistance - currentDistance) * 0.01; cameraDistance = Math.max(5, Math.min(100, cameraDistance + zoomDelta)); touchStateLocal.initialDistance = currentDistance; const midX = (touch1.clientX + touch2.clientX) / 2; const midY = (touch1.clientY + touch2.clientY) / 2; const deltaX = midX - panStartX; const deltaY = midY - panStartY; const panSpeed = 0.005; const panVector = new THREE.Vector3(-deltaX, deltaY, 0).multiplyScalar(panSpeed); panVector.applyQuaternion(camera.quaternion); cameraTarget.add(panVector); panStartX = midX; panStartY = midY; updateCameraPosition(); } e.preventDefault(); }); container.addEventListener('touchend', function(e) { isMovingModel = false; isRotating = false; isDraggingOrb = false; currentOrb = null; if (e.touches.length < 2) touchStateLocal.isTwoFinger = false; window.lastTouchX = null; window.lastTouchY = null; }); container.addEventListener('touchcancel', function() { isMovingModel = false; isRotating = false; isDraggingOrb = false; currentOrb = null; }); } function setupKeyboardControls() { document.addEventListener('keydown', function(e) { keys[e.key.toLowerCase()] = true; switch(e.key.toLowerCase()) { case 'r': if (controlMode === 'bone') resetSelectedBone(); else resetView(); break; case 'f': toggleWireframe(); break; case 'h': document.getElementById('help-overlay').style.display = document.getElementById('help-overlay').style.display === 'block' ? 'none' : 'block'; break; case 'c': if (controlMode !== 'bone') toggleControlMode(); break; case 'b': toggleBoneMode(); break; case '1': selectModel('both'); break; case '2': selectModel('model1'); break; case '3': selectModel('model2'); break; case 'l': toggleLightMode(); break; } }); document.addEventListener('keyup', function(e) { keys[e.key.toLowerCase()] = false; }); } function handleKeyboardInput() { if (controlMode === 'bone') return; const moveSpeed = 0.1; const rotationSpeed = 0.03; if (controlMode === 'model') { const targetModels = getTargetModels(); if (keys['w']) targetModels.forEach(model => { if (model) model.position.z -= moveSpeed; }); if (keys['s']) targetModels.forEach(model => { if (model) model.position.z += moveSpeed; }); if (keys['a']) targetModels.forEach(model => { if (model) model.position.x -= moveSpeed; }); if (keys['d']) targetModels.forEach(model => { if (model) model.position.x += moveSpeed; }); if (keys['q']) targetModels.forEach(model => { if (model) model.position.y += moveSpeed; }); if (keys['e']) targetModels.forEach(model => { if (model) model.position.y -= moveSpeed; }); if (keys['arrowup']) targetModels.forEach(model => { if (model) model.rotation.x -= rotationSpeed; }); if (keys['arrowdown']) targetModels.forEach(model => { if (model) model.rotation.x += rotationSpeed; }); if (keys['arrowleft']) targetModels.forEach(model => { if (model) model.rotation.y += rotationSpeed; }); if (keys['arrowright']) targetModels.forEach(model => { if (model) model.rotation.y -= rotationSpeed; }); } else if (controlMode === 'camera') { if (keys['w']) { const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion); cameraTarget.add(forward.multiplyScalar(moveSpeed)); updateCameraPosition(); } if (keys['s']) { const backward = new THREE.Vector3(0, 0, 1).applyQuaternion(camera.quaternion); cameraTarget.add(backward.multiplyScalar(moveSpeed)); updateCameraPosition(); } if (keys['a']) { const left = new THREE.Vector3(-1, 0, 0).applyQuaternion(camera.quaternion); cameraTarget.add(left.multiplyScalar(moveSpeed)); updateCameraPosition(); } if (keys['d']) { const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion); cameraTarget.add(right.multiplyScalar(moveSpeed)); updateCameraPosition(); } if (keys['q']) { cameraTarget.y += moveSpeed; updateCameraPosition(); } if (keys['e']) { cameraTarget.y -= moveSpeed; updateCameraPosition(); } if (keys['arrowup']) { cameraPhi = Math.max(0.1, cameraPhi - rotationSpeed); updateCameraPosition(); } if (keys['arrowdown']) { cameraPhi = Math.min(Math.PI - 0.1, cameraPhi + rotationSpeed); updateCameraPosition(); } if (keys['arrowleft']) { cameraTheta += rotationSpeed; updateCameraPosition(); } if (keys['arrowright']) { cameraTheta -= rotationSpeed; updateCameraPosition(); } } } function handleBoneDragStart(event, isTouch = false) { if (controlMode !== 'bone' || !selectedBone) return false; const mouse = new THREE.Vector2(); if (isTouch) { mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.touches[0].clientY / window.innerHeight) * 2 + 1; } else { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; } const raycasterLocal = new THREE.Raycaster(); raycasterLocal.setFromCamera(mouse, camera); const intersects = raycasterLocal.intersectObjects(boneHelpers); const hitSelectedBone = intersects.some(intersect => intersect.object.userData.bone === selectedBone); if (hitSelectedBone) { isDraggingBone = true; if (boneTransformMode === 'move') { const cameraDirection = new THREE.Vector3(); camera.getWorldDirection(cameraDirection); dragPlane.setFromNormalAndCoplanarPoint(cameraDirection, selectedBone.getWorldPosition(new THREE.Vector3())); raycasterLocal.ray.intersectPlane(dragPlane, dragStartPoint); dragStartBonePosition.copy(selectedBone.position); } else { dragStartBoneRotation.copy(selectedBone.rotation); dragStartPoint.set(mouse.x, mouse.y, 0); } event.preventDefault(); return true; } return false; } function handleBoneDrag(event, isTouch = false) { if (!isDraggingBone || !selectedBone) return; const mouse = new THREE.Vector2(); if (isTouch) { mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.touches[0].clientY / window.innerHeight) * 2 + 1; } else { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; } if (boneTransformMode === 'move') { const raycasterLocal = new THREE.Raycaster(); raycasterLocal.setFromCamera(mouse, camera); const dragPoint = new THREE.Vector3(); if (raycasterLocal.ray.intersectPlane(dragPlane, dragPoint)) { const delta = new THREE.Vector3().subVectors(dragPoint, dragStartPoint); selectedBone.position.copy(dragStartBonePosition).add(delta); } } else { const deltaX = mouse.x - dragStartPoint.x; const deltaY = mouse.y - dragStartPoint.y; const rotationSpeed = 2; selectedBone.rotation.x = dragStartBoneRotation.x + deltaY * rotationSpeed; selectedBone.rotation.y = dragStartBoneRotation.y + deltaX * rotationSpeed; } if (selectedModel) { selectedModel.skeleton.pose(); selectedModel.updateMatrixWorld(true); } updateBoneHelpers(); event.preventDefault(); } function handleBoneDragEnd() { isDraggingBone = false; } function updateBoneHelpers() { boneHelpers.forEach(helper => { const bone = helper.userData.bone; const worldPos = new THREE.Vector3(); bone.getWorldPosition(worldPos); helper.position.copy(worldPos); }); if (activeBoneModel) updateOrbPositions(); } function handleBoneSelection(e) { const touch = e.touches[0]; const mouse = new THREE.Vector2(); mouse.x = (touch.clientX / window.innerWidth) * 2 - 1; mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1; const raycasterLocal = new THREE.Raycaster(); raycasterLocal.setFromCamera(mouse, camera); const intersects = raycasterLocal.intersectObjects(boneHelpers); if (intersects.length > 0) { selectedBone = intersects[0].object.userData.bone; selectedModel = intersects[0].object.userData.model; updateBoneSelection(); updateBoneButtons(); } } function animate() { requestAnimationFrame(animate); handleKeyboardInput(); if (controlMode === 'bone' && activeBoneModel) updateOrbPositions(); renderer.render(scene, camera); } function init() { helper = new THREE.MMDAnimationHelper(); scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a1a2e); camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); updateCameraPosition(); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.getElementById("container").appendChild(renderer.domElement); scene.add(new THREE.AmbientLight(0xffffff, 0.3)); scene.add(new THREE.GridHelper(20, 20)); scene.add(new THREE.AxesHelper(5)); loader = new THREE.MMDLoader(); raycaster = new THREE.Raycaster(); mouseVec = new THREE.Vector2(); setupTouchControls(); setupMouseControls(); setupKeyboardControls(); setupLightControls(); loadModels(); window.addEventListener("resize", () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); document.getElementById("resetView").addEventListener("click", resetView); document.getElementById("toggleWireframe").addEventListener("click", toggleWireframe); document.getElementById("toggleMode").addEventListener("click", toggleControlMode); document.getElementById("toggleBones").addEventListener("click", toggleBoneMode); document.getElementById("reset-bones").addEventListener("click", resetSelectedBone); document.getElementById("move-bone").addEventListener("click", () => setBoneTransformMode('move')); document.getElementById("rotate-bone").addEventListener("click", () => setBoneTransformMode('rotate')); document.getElementById("reset-bone-transform").addEventListener("click", resetSelectedBone); document.getElementById('x-plus').addEventListener('click', () => moveBoneOnAxis('x', 1)); document.getElementById('x-minus').addEventListener('click', () => moveBoneOnAxis('x', -1)); document.getElementById('y-plus').addEventListener('click', () => moveBoneOnAxis('y', 1)); document.getElementById('y-minus').addEventListener('click', () => moveBoneOnAxis('y', -1)); document.getElementById('z-plus').addEventListener('click', () => moveBoneOnAxis('z', 1)); document.getElementById('z-minus').addEventListener('click', () => moveBoneOnAxis('z', -1)); document.querySelectorAll('.model-btn').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.model-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); currentModel = this.getAttribute('data-model'); selectModel(currentModel); }); }); document.querySelectorAll('.bone-btn').forEach(btn => { btn.addEventListener('click', function() { if (controlMode !== 'bone') return; const boneType = this.getAttribute('data-bone'); selectBoneByType(boneType); }); }); document.getElementById('reload-stor1').addEventListener('click', () => reloadStoredPose('stor1', model1)); document.getElementById('reload-stor2').addEventListener('click', () => reloadStoredPose('stor2', model2)); document.getElementById('reload-both').addEventListener('click', reloadBothStoredPoses); document.getElementById('generate-state').addEventListener('click', generateState); document.getElementById('move-up').addEventListener('click', moveModelsUp); document.getElementById('move-down').addEventListener('click', moveModelsDown); document.getElementById('help-btn').addEventListener('click', () => document.getElementById('help-overlay').style.display = 'block'); document.getElementById('close-help').addEventListener('click', () => document.getElementById('help-overlay').style.display = 'none'); document.getElementById('close-gesture-help').addEventListener('click', () => document.getElementById('mobile-gesture-info').style.display = 'none'); if (/Mobi|Android|iPhone|iPad/.test(navigator.userAgent)) setTimeout(() => document.getElementById('mobile-gesture-info').style.display = 'block', 1000); animate(); } init();