Generate Entry


[PoseModel1 loc] [PoseModel2 loc]


Generated Image

Wissy n Cea :)

Date: 2025-11-29-16:41



Generated Image

CarryUnderOneArm

Date: 2025-11-21-02:07



Generated Image

Princess carry over shldr view

Date: 2025-11-08-09:13



Generated Image

Yusaku riding FirewallDragon

Date: 2025-11-07-22:13



Generated Image

Fedis tech in a nutshell. Good Thing wissy is here => Result Img

Date: 2025-11-05-07:59



Generated Image

Standart TPose twrds e.o.

Date: 2025-11-02-07:20



src photostudio.html -- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>PMX + VPD Viewer</title> <script src="./libs/three.js"></script> <script src="./libs/mmdparser.min.js"></script> <script src="./libs/MMDAnimationHelper.js"></script> <script src="./libs/CCDIKSolver.js"></script> <script src="./libs/ammo.min.js"></script> <script src="./libs/TGALoader.js"></script> <script src="./libs/MMDLoader.js"></script> <style> *{margin:0;padding:0;box-sizing:border-box;touch-action:none} body{overflow:hidden;background:#1a1a2e;color:white;font-family:system-ui;height:100vh;width:100vw} #container{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1} #loading{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.7);padding:20px 30px;border-radius:10px;z-index:10;text-align:center;font-size:18px} #info{position:fixed;top:10px;left:10px;background:rgba(0,0,0,0.7);padding:12px;border-radius:10px;z-index:5;max-width:90%;font-size:14px;line-height:1.4} #info h1{font-size:16px;margin-bottom:8px} #controls{position:fixed;bottom:20px;left:0;width:100%;display:flex;justify-content:center;gap:15px;z-index:5;padding:0 20px} .control-btn{background:rgba(30,30,60,0.8);border:2px solid #4a4a8a;color:white;padding:12px 20px;border-radius:50px;font-size:16px;font-weight:600;cursor:pointer;box-shadow:0 4px 8px rgba(0,0,0,0.3);transition:all 0.2s;min-width:140px} .control-btn:active{background:rgba(60,60,100,0.9);transform:scale(0.95)} .control-btn.active{background:rgba(80,80,160,0.9);border-color:#8a8aff} #model-selector{position:fixed;top:80px;right:10px;background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:flex;flex-direction:column;gap:8px} .model-btn{background:rgba(40,40,80,0.8);border:1px solid #5a5aaa;color:white;padding:8px 12px;border-radius:6px;font-size:14px;cursor:pointer} .model-btn.active{background:rgba(80,80,160,0.9);border-color:#8a8aff} #help-overlay{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.9);padding:20px;border-radius:10px;z-index:20;max-width:90%;max-height:80%;overflow-y:auto;display:none} #help-overlay h2{margin-bottom:15px;text-align:center} .help-section{margin-bottom:15px} .help-section h3{margin-bottom:8px;color:#8a8aff} .help-key{display:inline-block;background:rgba(255,255,255,0.2);padding:2px 6px;border-radius:4px;margin:0 2px;font-family:monospace} #close-help{display:block;margin:15px auto 0;padding:8px 20px;background:#4a4a8a;border:none;border-radius:5px;color:white;cursor:pointer} #mode-indicator{position:fixed;top:10px;right:60px;background:rgba(0,0,0,0.7);padding:8px 12px;border-radius:6px;z-index:5;font-size:14px} #bone-controls{position:fixed;top:150px;left:10px;background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:none;flex-direction:column;gap:8px;max-width:200px} .bone-section{margin-bottom:10px} .bone-section h4{margin-bottom:5px;color:#8a8aff;font-size:12px} .bone-btn{background:rgba(60,60,100,0.8);border:1px solid #6a6aaa;color:white;padding:4px 8px;border-radius:4px;font-size:12px;cursor:pointer;margin:2px;width:calc(50% - 4px);display:inline-block;text-align:center} .bone-btn.active{background:rgba(100,100,200,0.9);border-color:#8a8aff} #selected-bone-info{position:fixed;top:50%;left:10px;background:rgba(0,0,0,0.8);padding:10px;border-radius:10px;z-index:5;font-size:12px;display:none} #bone-transform-controls{position:fixed;bottom:120px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:none;gap:10px} .transform-btn{background:rgba(60,60,100,0.8);border:1px solid #6a6aaa;color:white;padding:8px 12px;border-radius:6px;font-size:14px;cursor:pointer;min-width:80px} .transform-btn.active{background:rgba(100,100,200,0.9);border-color:#8a8aff} #gesture-help{position:fixed;bottom:180px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);padding:8px 12px;border-radius:6px;z-index:5;font-size:12px;text-align:center;display:none} #stored-pose-controls{position:fixed;top:80px;left:10px;background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:flex;flex-direction:column;gap:8px;max-width:200px} .stored-pose-section{margin-bottom:10px} .stored-pose-section h4{margin-bottom:5px;color:#8a8aff;font-size:12px} .stored-pose-btn{background:rgba(60,100,60,0.8);border:1px solid #6aaa6a;color:white;padding:6px 10px;border-radius:4px;font-size:12px;cursor:pointer;margin:2px;width:100%;text-align:center} .stored-pose-btn:active{background:rgba(80,140,80,0.9);transform:scale(0.95)} #state-controls{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:flex;flex-direction:column;gap:10px;width:90%;max-width:600px} .state-btn{background:rgba(100,60,100,0.8);border:1px solid #8a6a8a;color:white;padding:8px 12px;border-radius:6px;font-size:14px;cursor:pointer;min-width:100px} .state-btn:active{background:rgba(140,80,140,0.9);transform:scale(0.95)} #state-textbox{background:rgba(0,0,0,0.8);border:1px solid #4a4a8a;border-radius:6px;color:white;padding:8px;font-size:12px;font-family:monospace;width:100%;min-height:60px;resize:vertical;display:none} #help-btn{position:fixed;top:10px;right:10px;background:rgba(30,30,60,0.8);border:2px solid #4a4a8a;color:white;padding:8px 12px;border-radius:50%;width:40px;height:40px;cursor:pointer;z-index:5;font-size:18px;display:flex;align-items:center;justify-content:center} .bone-helper{cursor:pointer;transition:all 0.2s} .bone-helper:hover{transform:scale(1.2)} #mobile-gesture-info{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.9);padding:20px;border-radius:10px;z-index:15;text-align:center;display:none;max-width:90%} .gesture-item{margin:10px 0;padding:10px;background:rgba(255,255,255,0.1);border-radius:5px} @media (max-width:480px){#info{font-size:12px;padding:10px}#info h1{font-size:14px}.control-btn{padding:10px 15px;font-size:14px;min-width:120px}#mode-indicator{top:60px;right:10px}#bone-controls{top:120px;max-width:180px}#bone-transform-controls{bottom:100px;flex-wrap:wrap;justify-content:center}.transform-btn{padding:6px 10px;font-size:12px;min-width:70px}#gesture-help{bottom:160px;font-size:11px}#stored-pose-controls{top:60px;max-width:180px}#state-controls{bottom:70px;flex-wrap:wrap;justify-content:center}.state-btn{padding:6px 10px;font-size:12px;min-width:90px}#state-textbox{font-size:11px;min-height:50px}} </style> </head> <body> <div id="container"></div> <div id="loading">Loading models...</div> <div id="info"> <h1>PMX + VPD Viewer</h1> <p>Touch models to move them, empty space to rotate/camera</p> <p id="modelInfo"></p> </div> <div id="mode-indicator">Mode: Model Control</div> <button id="help-btn">?</button> <div id="selected-bone-info">No bone selected</div> <div id="mobile-gesture-info"> <h3>Mobile Gestures Guide</h3> <div class="gesture-item"><strong>Touch Model + Drag:</strong> Move model</div> <div class="gesture-item"><strong>Touch Empty Space + Drag:</strong> Rotate model (Model Mode) / Orbit camera (Camera Mode)</div> <div class="gesture-item"><strong>Two Finger Drag:</strong> Pan camera</div> <div class="gesture-item"><strong>Pinch:</strong> Zoom camera</div> <div class="gesture-item"><strong>Double Tap Bone:</strong> Select bone (Bone Mode)</div> <div class="gesture-item"><strong>Long Press + Drag Bone:</strong> Move bone (Bone Mode)</div> <button id="close-gesture-help" style="margin-top:15px;padding:8px 16px;background:#4a4a8a;color:white;border:none;border-radius:5px;cursor:pointer">Got it!</button> </div> <div id="state-controls"> <button id="generate-state" class="state-btn">Generate State URL</button> <textarea id="state-textbox" placeholder="Click 'Generate State URL' to create a shareable URL with current camera and model positions"></textarea> </div> <div id="stored-pose-controls"> <div class="stored-pose-section"> <h4>Stored Poses</h4> <button id="reload-stor1" class="stored-pose-btn">Reload Pose 1</button> <button id="reload-stor2" class="stored-pose-btn">Reload Pose 2</button> <button id="reload-both" class="stored-pose-btn">Reload Both</button> </div> </div> <div id="bone-controls"> <div class="bone-section"><h4>Head & Neck</h4><button class="bone-btn" data-bone="neck">Neck</button><button class="bone-btn" data-bone="head">Head</button></div> <div class="bone-section"><h4>Arms & Hands</h4><button class="bone-btn" data-bone="shoulder_l">Left Shoulder</button><button class="bone-btn" data-bone="shoulder_r">Right Shoulder</button><button class="bone-btn" data-bone="arm_l">Left Arm</button><button class="bone-btn" data-bone="arm_r">Right Arm</button><button class="bone-btn" data-bone="hand_l">Left Hand</button><button class="bone-btn" data-bone="hand_r">Right Hand</button></div> <div class="bone-section"><h4>Legs & Feet</h4><button class="bone-btn" data-bone="leg_l">Left Leg</button><button class="bone-btn" data-bone="leg_r">Right Leg</button><button class="bone-btn" data-bone="knee_l">Left Knee</button><button class="bone-btn" data-bone="knee_r">Right Knee</button><button class="bone-btn" data-bone="foot_l">Left Foot</button><button class="bone-btn" data-bone="foot_r">Right Foot</button></div> <div class="bone-section"><h4>Spine</h4><button class="bone-btn" data-bone="spine">Spine</button><button class="bone-btn" data-bone="waist">Waist</button></div> <button id="reset-bones" class="control-btn" style="margin-top:10px;min-width:auto">Reset Bones</button> </div> <div id="bone-transform-controls"> <button id="move-bone" class="transform-btn active">Move</button> <button id="rotate-bone" class="transform-btn">Rotate</button> <button id="reset-bone-transform" class="transform-btn">Reset</button> </div> <div id="gesture-help"><div>👆 Touch model to move, empty space to rotate</div></div> <div id="help-overlay"> <h2>Controls Guide</h2> <div class="help-section"><h3>Mobile Touch Controls</h3><p><strong>Touch Model + Drag:</strong> Move model</p><p><strong>Touch Empty Space + Drag:</strong> Rotate model (Model Mode) / Orbit camera (Camera Mode)</p><p><strong>Two Finger Drag:</strong> Pan camera</p><p><strong>Pinch:</strong> Zoom camera</p><p><strong>Double Tap Bone:</strong> Select bone (Bone Mode)</p><p><strong>Long Press + Drag Bone:</strong> Move bone (Bone Mode)</p></div> <div class="help-section"><h3>Control Modes</h3><p><span class="help-key">Model Mode</span> - Move and rotate models</p><p><span class="help-key">Camera Mode</span> - Control camera view</p><p><span class="help-key">Bone Mode</span> - Edit individual bone positions</p></div> <div class="help-section"><h3>Bone Editing (Bone Mode Only)</h3><p><span class="help-key">Tap Bone</span> - Select bone</p><p><span class="help-key">Long Press + Drag</span> - Move selected bone</p><p><span class="help-key">Two Finger Rotate</span> - Rotate selected bone</p></div> <div class="help-section"><h3>Stored Poses</h3><p><span class="help-key">Reload Pose 1</span> - Re-fetch and apply stor1 to Model 1</p><p><span class="help-key">Reload Pose 2</span> - Re-fetch and apply stor2 to Model 2</p><p><span class="help-key">Reload Both</span> - Re-fetch and apply both poses</p></div> <div class="help-section"><h3>State Management</h3><p><span class="help-key">Generate State URL</span> - Create URL with current scene state (camera position, model positions)</p><p><span class="help-key">Text Box</span> - Copy the generated URL manually to share your scene</p></div> <button id="close-help">Close</button> </div> <div id="model-selector"> <button class="model-btn active" data-model="both">Both Models</button> <button class="model-btn" data-model="model1">Model 1</button> <button class="model-btn" data-model="model2">Model 2</button> </div> <div id="controls"> <button id="toggleMode" class="control-btn">Camera Mode</button> <button id="toggleBones" class="control-btn">Bone Mode</button> <button id="resetView" class="control-btn">Reset View</button> <button id="toggleWireframe" class="control-btn">Wireframe</button> </div> <script> // Get URL parameters const urlParams = new URLSearchParams(window.location.search); const pmxPath1 = urlParams.get("pmx"); const pmxPath2 = urlParams.get("pmx2"); const vpdPath1 = urlParams.get("vpd"); const vpdPath2 = urlParams.get("vpd2"); // State parameters - simple format const stateCam = urlParams.get("cam"); const stateM1 = urlParams.get("m1"); const stateM2 = urlParams.get("m2"); let scene, camera, renderer; let model1, model2; let loader; // Input control variables let isRotating = false; let isMovingModel = false; let isPanning = false; let rotateStartX = 0, rotateStartY = 0; let moveStartX = 0, moveStartY = 0; let panStartX = 0, panStartY = 0; let currentModel = 'both'; let controlMode = 'model'; // Keyboard state const keys = {}; // Camera control variables let cameraTarget = new THREE.Vector3(0, 0, 0); let cameraDistance = 25; let cameraPhi = Math.PI / 6; let cameraTheta = 0; // Touch state let touchState = { isTwoFinger: false, initialDistance: 0, initialPan: { x: 0, y: 0 }, lastTapTime: 0, isLongPress: false, longPressTimer: null, isTouchingModel: false }; // Bone editing variables let selectedBone = null; let selectedModel = null; let boneHelpers = []; let isDraggingBone = false; let boneTransformMode = 'move'; let dragPlane = new THREE.Plane(); let dragStartPoint = new THREE.Vector3(); let dragStartBonePosition = new THREE.Vector3(); let dragStartBoneRotation = new THREE.Euler(); const boneMappings = { 'neck': ['首', 'neck', 'kubi'], 'head': ['頭', 'head', 'atama'], 'shoulder_l': ['左肩', 'shoulder_l', 'left shoulder', '左腕'], 'shoulder_r': ['右肩', 'shoulder_r', 'right shoulder', '右腕'], 'arm_l': ['左腕', 'arm_l', 'left arm', '左上腕'], 'arm_r': ['右腕', 'arm_r', 'right arm', '右上腕'], 'hand_l': ['左手', 'hand_l', 'left hand', '左手首'], 'hand_r': ['右手', 'hand_r', 'right hand', '右手首'], 'leg_l': ['左足', 'leg_l', 'left leg', '左大腿'], 'leg_r': ['右足', 'leg_r', 'right leg', '右大腿'], 'knee_l': ['左ひざ', 'knee_l', 'left knee', '左ひざ'], 'knee_r': ['右ひざ', 'knee_r', 'right knee', '右ひざ'], 'foot_l': ['左足首', 'foot_l', 'left foot', '左足先'], 'foot_r': ['右足首', 'foot_r', 'right foot', '右足先'], 'spine': ['上半身', 'spine', 'upper body', '上半身2'], 'waist': ['下半身', 'waist', 'lower body', '腰'] }; // Essential functions that were missing function updateCameraPosition() { const spherical = new THREE.Spherical(cameraDistance, cameraPhi, cameraTheta); const position = new THREE.Vector3(); position.setFromSpherical(spherical); position.add(cameraTarget); camera.position.copy(position); camera.lookAt(cameraTarget); } function setupTouchControls() { const container = document.getElementById('container'); const gestureHelp = document.getElementById('gesture-help'); container.addEventListener('touchstart', function(e) { const now = Date.now(); const isDoubleTap = (now - touchState.lastTapTime) < 300; touchState.lastTapTime = now; gestureHelp.style.display = 'block'; setTimeout(() => gestureHelp.style.display = 'none', 2000); 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; const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const targetModels = getTargetModels(); let hitModel = false; for (const model of targetModels) { if (!model) continue; const intersects = raycaster.intersectObject(model, true); if (intersects.length > 0) { hitModel = true; break; } } if (hitModel && controlMode === 'model') { touchState.isTouchingModel = true; isMovingModel = true; moveStartX = touch.clientX; moveStartY = touch.clientY; } else if (controlMode === 'bone' && isDoubleTap) { handleBoneSelection(e); e.preventDefault(); return; } else { touchState.isTouchingModel = false; isRotating = true; rotateStartX = touch.clientX; rotateStartY = touch.clientY; if (controlMode === 'bone') { touchState.longPressTimer = setTimeout(() => { touchState.isLongPress = true; if (handleBoneDragStart(e, true)) { e.preventDefault(); return; } }, 500); } } } else if (e.touches.length === 2) { touchState.isTwoFinger = true; const touch1 = e.touches[0]; const touch2 = e.touches[1]; touchState.initialDistance = Math.hypot( touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY ); touchState.initialPan.x = (touch1.clientX + touch2.clientX) / 2; touchState.initialPan.y = (touch1.clientY + touch2.clientY) / 2; panStartX = touchState.initialPan.x; panStartY = touchState.initialPan.y; } e.preventDefault(); }); container.addEventListener('touchmove', function(e) { if (controlMode === 'bone' && isDraggingBone) { handleBoneDrag(e, true); return; } if (e.touches.length === 1) { const touch = e.touches[0]; if (touchState.isTouchingModel && controlMode === 'model' && isMovingModel) { const deltaX = touch.clientX - moveStartX; const deltaY = touch.clientY - moveStartY; const moveSpeed = 0.05; const targetModels = getTargetModels(); targetModels.forEach(model => { if (model) { model.position.x += deltaX * moveSpeed; model.position.z -= deltaY * moveSpeed; } }); moveStartX = touch.clientX; moveStartY = touch.clientY; } else if (isRotating) { const deltaX = touch.clientX - rotateStartX; const deltaY = touch.clientY - rotateStartY; if (controlMode === 'model') { const rotationSpeed = 0.01; const targetModels = getTargetModels(); targetModels.forEach(model => { if (model) { model.rotation.y += deltaX * rotationSpeed; model.rotation.x += deltaY * rotationSpeed; } }); } else if (controlMode === 'camera') { const orbitSpeed = 0.01; cameraTheta -= deltaX * orbitSpeed; cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi + deltaY * orbitSpeed)); updateCameraPosition(); } rotateStartX = touch.clientX; rotateStartY = touch.clientY; } } else if (e.touches.length === 2 && touchState.isTwoFinger) { const touch1 = e.touches[0]; const touch2 = e.touches[1]; const currentDistance = Math.hypot( touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY ); const zoomDelta = (touchState.initialDistance - currentDistance) * 0.01; cameraDistance = Math.max(5, Math.min(100, cameraDistance + zoomDelta)); touchState.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) { if (touchState.longPressTimer) { clearTimeout(touchState.longPressTimer); touchState.longPressTimer = null; } if (controlMode === 'bone') { handleBoneDragEnd(); touchState.isLongPress = false; } isMovingModel = false; isRotating = false; touchState.isTouchingModel = false; if (e.touches.length < 2) { touchState.isTwoFinger = false; } }); } function setupMouseControls() { const container = document.getElementById('container'); let isMouseDown = false; let isRightClick = false; let isMiddleClick = false; let lastMouseX = 0; let 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; const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const targetModels = getTargetModels(); let hitModel = false; for (const model of targetModels) { if (!model) continue; const intersects = raycaster.intersectObject(model, true); if (intersects.length > 0) { hitModel = true; break; } } if (hitModel && controlMode === 'model' && e.button === 0) { isMovingModel = true; moveStartX = e.clientX; moveStartY = e.clientY; } else { if (controlMode === 'bone') { if (handleBoneDragStart(e, false)) { return; } if (e.button === 0) { const intersects = raycaster.intersectObjects(boneHelpers); if (intersects.length > 0) { selectedBone = intersects[0].object.userData.bone; selectedModel = intersects[0].object.userData.model; updateBoneSelection(); updateBoneButtons(); } } } isRotating = true; rotateStartX = e.clientX; rotateStartY = e.clientY; } e.preventDefault(); }); container.addEventListener('mousemove', function(e) { 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') { const moveSpeed = 0.02; const targetModels = getTargetModels(); targetModels.forEach(model => { if (model) { model.position.x += deltaX * moveSpeed; model.position.z -= deltaY * moveSpeed; } }); } else if (isRotating) { if (controlMode === 'model') { const targetModels = getTargetModels(); if (isMiddleClick) { const panSpeed = 0.01; const panVector = new THREE.Vector3(-deltaX, deltaY, 0).multiplyScalar(panSpeed); panVector.applyQuaternion(camera.quaternion); cameraTarget.add(panVector); updateCameraPosition(); } else if (isRightClick) { const moveSpeed = 0.02; targetModels.forEach(model => { if (model) { model.position.x += deltaX * moveSpeed; model.position.y -= deltaY * moveSpeed; } }); } else { const rotationSpeed = 0.01; targetModels.forEach(model => { if (model) { model.rotation.y += deltaX * rotationSpeed; model.rotation.x += deltaY * rotationSpeed; } }); } } 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 { const orbitSpeed = 0.01; cameraTheta -= deltaX * orbitSpeed; cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi + deltaY * orbitSpeed)); updateCameraPosition(); } } } lastMouseX = e.clientX; lastMouseY = e.clientY; }); container.addEventListener('mouseup', function() { isMouseDown = false; isMovingModel = false; isRotating = false; if (controlMode === 'bone') { handleBoneDragEnd(); } }); container.addEventListener('wheel', function(e) { if (controlMode !== 'bone') { const zoomSpeed = 0.5; cameraDistance = Math.max(5, Math.min(100, cameraDistance + e.deltaY * 0.01 * zoomSpeed)); updateCameraPosition(); e.preventDefault(); } }); container.addEventListener('contextmenu', function(e) { e.preventDefault(); }); } 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; } }); document.addEventListener('keyup', function(e) { keys[e.key.toLowerCase()] = false; }); } function handleKeyboardInput() { if (controlMode === 'bone') { handleBoneEditing(); 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(); } } } // State management functions function generateState() { const params = new URLSearchParams(); if (pmxPath1) params.set('pmx', pmxPath1); if (pmxPath2) params.set('pmx2', pmxPath2); if (vpdPath1) params.set('vpd', vpdPath1); if (vpdPath2) params.set('vpd2', vpdPath2); // Camera state: targetX,targetY,targetZ,distance,phi,theta const camState = `${cameraTarget.x.toFixed(2)},${cameraTarget.y.toFixed(2)},${cameraTarget.z.toFixed(2)},${cameraDistance.toFixed(2)},${cameraPhi.toFixed(4)},${cameraTheta.toFixed(4)}`; params.set('cam', camState); // Model 1 state: posX,posY,posZ,rotX,rotY,rotZ if (model1) { const m1State = `${model1.position.x.toFixed(2)},${model1.position.y.toFixed(2)},${model1.position.z.toFixed(2)},${model1.rotation.x.toFixed(4)},${model1.rotation.y.toFixed(4)},${model1.rotation.z.toFixed(4)}`; params.set('m1', m1State); } // Model 2 state if (model2) { const m2State = `${model2.position.x.toFixed(2)},${model2.position.y.toFixed(2)},${model2.position.z.toFixed(2)},${model2.rotation.x.toFixed(4)},${model2.rotation.y.toFixed(4)},${model2.rotation.z.toFixed(4)}`; params.set('m2', m2State); } // Add stored poses to URL if they exist const storedPose1 = localStorage.getItem('stor1'); const storedPose2 = localStorage.getItem('stor2'); if (storedPose1) { params.set('stor1', encodeURIComponent(storedPose1)); } if (storedPose2) { params.set('stor2', encodeURIComponent(storedPose2)); } const stateURL = `${window.location.origin}${window.location.pathname}?${params.toString()}`; // Show URL in textbox const textbox = document.getElementById('state-textbox'); textbox.value = stateURL; textbox.style.display = 'block'; textbox.select(); return stateURL; } // Also update the loadStateFromURL function to handle stored poses from URL function loadStateFromURL() { if (stateCam) { const parts = stateCam.split(','); if (parts.length === 6) { cameraTarget.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2])); cameraDistance = parseFloat(parts[3]); cameraPhi = parseFloat(parts[4]); cameraTheta = parseFloat(parts[5]); updateCameraPosition(); } } // Load stored poses from URL parameters const urlStor1 = urlParams.get('stor1'); const urlStor2 = urlParams.get('stor2'); if (urlStor1) { try { const decodedStor1 = decodeURIComponent(urlStor1); // Validate it's valid JSON before storing JSON.parse(decodedStor1); localStorage.setItem('stor1', decodedStor1); } catch (error) { console.error('Invalid stor1 data in URL:', error); } } if (urlStor2) { try { const decodedStor2 = decodeURIComponent(urlStor2); // Validate it's valid JSON before storing JSON.parse(decodedStor2); localStorage.setItem('stor2', decodedStor2); } catch (error) { console.error('Invalid stor2 data in URL:', error); } } } // Update the applyModelStates function to also apply stored poses function applyModelStates() { if (stateM1 && model1) { const parts = stateM1.split(','); if (parts.length === 6) { model1.position.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2])); model1.rotation.set(parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5])); } } if (stateM2 && model2) { const parts = stateM2.split(','); if (parts.length === 6) { model2.position.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2])); model2.rotation.set(parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5])); } } // Apply stored poses from localStorage (which might have been loaded from URL) setTimeout(() => { applyStoredPosesOnLoad(); }, 100); } function applyModelStates() { if (stateM1 && model1) { const parts = stateM1.split(','); if (parts.length === 6) { model1.position.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2])); model1.rotation.set(parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5])); } } if (stateM2 && model2) { const parts = stateM2.split(','); if (parts.length === 6) { model2.position.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2])); model2.rotation.set(parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5])); } } } // Rest of the essential functions function getTargetModels() { switch(currentModel) { case 'model1': return [model1]; case 'model2': return [model2]; default: return [model1, model2]; } } 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); 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); 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) { console.log("Loading VPD:", vpdPath); loader.loadVPD(vpdPath, false, function(vpd) { console.log("VPD loaded successfully:", vpd); requestAnimationFrame(() => { try { helper.pose(model, vpd); console.log("Pose applied successfully to model"); } 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" : "") + "<br>"; if (model2) info += "Model 2 loaded" + (vpdPath2 ? " + Pose 2" : "") + "<br>"; 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"; // Always try to apply stored poses, regardless of URL parameters 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 onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } 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'; boneHelpers.forEach(helper => scene.remove(helper)); boneHelpers = []; selectedBone = null; selectedModel = null; } else { controlMode = 'bone'; document.getElementById('bone-controls').style.display = 'flex'; document.getElementById('bone-transform-controls').style.display = 'flex'; const targetModels = getTargetModels(); selectedModel = targetModels[0] || null; setupBoneHelpers(); 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'); } }); updateSelectedModel(); } function updateSelectedModel() { if (controlMode === 'bone') { const targetModels = getTargetModels(); selectedModel = targetModels[0] || null; if (selectedModel && selectedBone) { const boneName = selectedBone.name; const newBone = findBoneInModel(selectedModel, boneName); selectedBone = newBone; updateBoneSelection(); } } } 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 }; helper.className = 'bone-helper'; 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 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 raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const intersects = raycaster.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())); raycaster.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 raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const dragPoint = new THREE.Vector3(); if (raycaster.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 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 raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(boneHelpers); if (intersects.length > 0) { selectedBone = intersects[0].object.userData.bone; selectedModel = intersects[0].object.userData.model; updateBoneSelection(); updateBoneButtons(); } } function handleBoneEditing() { if (!selectedBone || controlMode !== 'bone') return; const moveSpeed = 0.05; const rotationSpeed = 0.03; if (keys['w']) selectedBone.position.z -= moveSpeed; if (keys['s']) selectedBone.position.z += moveSpeed; if (keys['a']) selectedBone.position.x -= moveSpeed; if (keys['d']) selectedBone.position.x += moveSpeed; if (keys['q']) selectedBone.position.y += moveSpeed; if (keys['e']) selectedBone.position.y -= moveSpeed; if (keys['arrowup']) selectedBone.rotation.x -= rotationSpeed; if (keys['arrowdown']) selectedBone.rotation.x += rotationSpeed; if (keys['arrowleft']) selectedBone.rotation.y += rotationSpeed; if (keys['arrowright']) selectedBone.rotation.y -= rotationSpeed; if (selectedModel) { selectedModel.skeleton.pose(); selectedModel.updateMatrixWorld(true); } updateBoneHelpers(); } function updateBoneHelpers() { boneHelpers.forEach(helper => { const bone = helper.userData.bone; const worldPos = new THREE.Vector3(); bone.getWorldPosition(worldPos); helper.position.copy(worldPos); }); } function resetSelectedBone() { if (selectedBone) { selectedBone.position.set(0, 0, 0); selectedBone.rotation.set(0, 0, 0); selectedBone.scale.set(1, 1, 1); if (selectedModel) { selectedModel.skeleton.pose(); selectedModel.updateMatrixWorld(true); } updateBoneHelpers(); } } 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 animate() { requestAnimationFrame(animate); handleKeyboardInput(); renderer.render(scene, camera); } // Initialize the application - MOVE THIS TO THE END 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); document.getElementById("container").appendChild(renderer.domElement); scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(10, 20, 15); scene.add(dirLight); scene.add(new THREE.GridHelper(20, 20)); scene.add(new THREE.AxesHelper(5)); loader = new THREE.MMDLoader(); setupTouchControls(); setupMouseControls(); setupKeyboardControls(); loadModels(); window.addEventListener("resize", onWindowResize); 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.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'); updateSelectedModel(); }); }); 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('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(); } // Finally, call init after all functions are defined init(); </script> </body> </html> pose2vpd.html -- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>MMD 3D Pose Editor</title> <style> body { margin: 0; overflow: hidden; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; touch-action: none; -webkit-tap-highlight-color: transparent; } canvas { display: block; cursor: grab; touch-action: none; } canvas.orb-dragging { cursor: grabbing !important; } #ui { position: absolute; top: 10px; left: 10px; background: rgba(25, 25, 35, 0.85); color: white; padding: 20px; border-radius: 12px; width: 280px; z-index: 100; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } #ui h2 { margin-top: 0; color: #6c63ff; text-align: center; font-weight: 600; font-size: 1.4em; margin-bottom: 20px; } button { width: 100%; padding: 12px; margin-bottom: 12px; border-radius: 6px; border: none; background: linear-gradient(135deg, #6c63ff 0%, #4a44b5 100%); color: white; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s; min-height: 44px; } button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(108, 99, 255, 0.4); } #reset-btn { background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); } #reset-btn:hover { box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4); } #instructions { font-size: 12px; color: #8888aa; margin-top: 15px; line-height: 1.5; text-align: center; } #loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 18px; z-index: 100; background: rgba(25, 25, 35, 0.9); padding: 20px 30px; border-radius: 10px; text-align: center; } .footer { display: flex; justify-content: space-between; margin-top: 15px; font-size: 11px; color: #666; } .camera-controls { margin-top: 15px; padding: 15px; background: rgba(42, 42, 58, 0.5); border-radius: 8px; } .control-hint { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 11px; } .control-key { background: rgba(108, 99, 255, 0.3); padding: 2px 6px; border-radius: 3px; } #axis-controls { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: none; z-index: 200; } .axis-btn { position: absolute; width: 44px; height: 44px; border-radius: 50%; border: none; color: white; font-size: 18px; font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; touch-action: manipulation; } .axis-btn:hover { transform: scale(1.2); } #x-plus { background: #ff4444; top: -60px; left: 0; } #x-minus { background: #ff4444; top: 60px; left: 0; } #y-plus { background: #44ff44; top: 0; left: -60px; } #y-minus { background: #44ff44; top: 0; left: 60px; } #z-plus { background: #4444ff; top: -40px; left: -40px; } #z-minus { background: #4444ff; top: 40px; left: 40px; } .mode-toggle { display: flex; margin-bottom: 15px; border-radius: 6px; overflow: hidden; background: rgba(42, 42, 58, 0.5); } .mode-btn { flex: 1; padding: 10px; background: transparent; border: none; color: #8888aa; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.3s; min-height: auto; } .mode-btn.active { background: #6c63ff; color: white; } #control-mode-indicator { text-align: center; margin-top: 10px; font-size: 12px; color: #a0a0c0; } .storage-buttons { display: flex; gap: 10px; margin-bottom: 12px; } .storage-buttons button { flex: 1; margin-bottom: 0; } #stor1-btn { background: linear-gradient(135deg, #ffa726 0%, #f57c00 100%); } #stor2-btn { background: linear-gradient(135deg, #66bb6a 0%, #388e3c 100%); } #load-buttons { display: flex; gap: 10px; margin-bottom: 12px; } #load-buttons button { flex: 1; margin-bottom: 0; } #load1-btn { background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%); } #load2-btn { background: linear-gradient(135deg, #81c784 0%, #4caf50 100%); } #export-btn { background: linear-gradient(135deg, #ab47bc 0%, #8e24aa 100%); } #export-btn:hover { box-shadow: 0 4px 12px rgba(171, 71, 188, 0.4); } .notification { position: fixed; top: 20px; right: 20px; background: rgba(25, 25, 35, 0.95); color: white; padding: 15px 20px; border-radius: 8px; border-left: 4px solid #6c63ff; z-index: 1000; transform: translateX(400px); transition: transform 0.3s ease; backdrop-filter: blur(10px); max-width: 300px; } .notification.show { transform: translateX(0); } .mobile-controls { position: absolute; bottom: 20px; right: 20px; display: flex; flex-direction: column; gap: 10px; z-index: 50; } .mobile-control-btn { width: 50px; height: 50px; border-radius: 50%; border: none; background: rgba(108, 99, 255, 0.8); color: white; font-size: 20px; display: flex; align-items: center; justify-content: center; cursor: pointer; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2); touch-action: manipulation; } #mobile-ui-toggle { position: absolute; top: 20px; right: 20px; width: 50px; height: 50px; border-radius: 50%; border: none; background: rgba(25, 25, 35, 0.8); color: white; font-size: 20px; z-index: 101; backdrop-filter: blur(10px); display: none; align-items: center; justify-content: center; cursor: pointer; } @media (max-width: 768px) { #ui { width: calc(100% - 40px); max-height: 70vh; overflow-y: auto; } #mobile-ui-toggle { display: flex; } .mobile-controls { display: flex; } } </style> <script src="./libs/three.js"></script> <script src="./libs/mmdparser.min.js"></script> <script src="./libs/ammo.min.js"></script> <script src="./libs/TGALoader.js"></script> <script src="./libs/CCDIKSolver.js"></script> <script src="./libs/MMDLoader.js"></script> <script src="./libs/MMDAnimationHelper.js"></script> </head> <body> <div id="loading"> <div>Loading MMD Model...</div> <div style="margin-top:10px;font-size:14px;" id="loading-progress">0%</div> </div> <button id="mobile-ui-toggle">☰</button> <div id="ui" style="display: none;"> <h2>MMD 3D Pose Editor</h2> <div class="mode-toggle"> <button class="mode-btn active" id="rotation-mode">Rotation</button> <button class="mode-btn" id="position-mode">Position</button> </div> <button id="reset-btn">Reset All Poses</button> <div class="storage-buttons"> <button id="stor1-btn">Store Pose 1</button> <button id="stor2-btn">Store Pose 2</button> </div> <div id="load-buttons"> <button id="load1-btn">Load Pose 1</button> <button id="load2-btn">Load Pose 2</button> </div> <button id="export-btn">Export as VPD</button> <div id="control-mode-indicator">Current Mode: Rotation</div> <div class="camera-controls"> <div style="text-align: center; margin-bottom: 10px; color: #a0a0c0; font-weight: 500;">Camera Controls</div> <div class="control-hint"> <span>Orbit Camera:</span> <span class="control-key">Right Drag</span> </div> <div class="control-hint"> <span>Zoom:</span> <span class="control-key">Scroll Wheel</span> </div> <div class="control-hint"> <span>Pan:</span> <span class="control-key">Middle Drag</span> </div> <div class="control-hint"> <span>Reset Camera:</span> <span class="control-key">Double Click</span> </div> </div> <div id="instructions"> <p>Click and drag the colored orbs to pose the model</p> <p>Red: Rotation • Green: Position • Blue: Special</p> <p><strong>Left Click:</strong> Drag Orbs • <strong>Right Click:</strong> Orbit Camera</p> <p><strong>Click Orb:</strong> Show Axis Controls</p> </div> <div class="footer"> <span>Magic Pose Editor</span> <span>v1.2</span> </div> </div> <div id="axis-controls"> <button class="axis-btn" id="x-plus">X+</button> <button class="axis-btn" id="x-minus">X-</button> <button class="axis-btn" id="y-plus">Y+</button> <button class="axis-btn" id="y-minus">Y-</button> <button class="axis-btn" id="z-plus">Z+</button> <button class="axis-btn" id="z-minus">Z-</button> </div> <div class="mobile-controls"> <button class="mobile-control-btn" id="mobile-orbit">🔄</button> <button class="mobile-control-btn" id="mobile-zoom-in">+</button> <button class="mobile-control-btn" id="mobile-zoom-out">-</button> <button class="mobile-control-btn" id="mobile-reset-cam">📷</button> </div> <div id="notification" class="notification"></div> <script> // Mobile detection and variables let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); let touchStartTime = 0; let lastTouchX = 0; let lastTouchY = 0; let isTouchOrbiting = false; let isTouchDraggingOrb = false; let touchOrbStartTime = 0; let touchOrb = null; let scene, camera, renderer, helper, model; let boneOrbs = []; let raycaster, mouse; let isDraggingOrb = false; let currentOrb = null; let orbGroup; let controls; let axisControls; let selectedOrb = null; let controlMode = 'rotation'; // 'rotation' or 'position' const ESSENTIAL_BONES = { '頭': 'head', '首': 'neck', '左腕': 'left_arm', '右腕': 'right_arm', '左ひじ': 'left_elbow', '右ひじ': 'right_elbow', '左手首': 'left_wrist', '右手首': 'right_wrist', '左足': 'left_leg_upper', '右足': 'right_leg_upper', '左ひざ': 'left_knee', '右ひざ': 'right_knee', '左足首': 'left_ankle', '右足首': 'right_ankle', '左目': 'left_eye', '右目': 'right_eye' }; function radToDeg(radians) { return radians * (180 / Math.PI); } function degToRad(degrees) { return degrees * (Math.PI / 180); } function showNotification(message, duration = 3000) { const notification = document.getElementById('notification'); notification.textContent = message; notification.classList.add('show'); setTimeout(() => { notification.classList.remove('show'); }, duration); } class SimpleOrbitControls { constructor(camera, domElement) { this.camera = camera; this.domElement = domElement; this.enabled = true; this.target = new THREE.Vector3(0, 10, 0); this.minDistance = 5; this.maxDistance = 100; this.minPolarAngle = 0; this.maxPolarAngle = Math.PI; this.spherical = new THREE.Spherical().setFromVector3( new THREE.Vector3().subVectors(camera.position, this.target) ); this.sphericalDelta = new THREE.Spherical(); this.scale = 1; this.panOffset = new THREE.Vector3(); this.zoomChanged = false; this.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 }; this.state = -1; this.domElement.addEventListener('contextmenu', this.onContextMenu.bind(this)); this.domElement.addEventListener('mousedown', this.onMouseDown.bind(this)); this.domElement.addEventListener('wheel', this.onMouseWheel.bind(this)); this.domElement.addEventListener('dblclick', this.onDoubleClick.bind(this)); // Touch events for mobile this.domElement.addEventListener('touchstart', this.onTouchStart.bind(this)); this.domElement.addEventListener('touchmove', this.onTouchMove.bind(this)); this.domElement.addEventListener('touchend', this.onTouchEnd.bind(this)); this.update(); } onContextMenu(event) { event.preventDefault(); } onMouseDown(event) { if (!this.enabled) return; if (event.button !== this.mouseButtons.RIGHT && event.button !== this.mouseButtons.MIDDLE) { return; } event.preventDefault(); switch (event.button) { case this.mouseButtons.RIGHT: this.state = 0; break; case this.mouseButtons.MIDDLE: this.state = 2; break; } if (this.state !== -1) { document.addEventListener('mousemove', this.onMouseMove.bind(this)); document.addEventListener('mouseup', this.onMouseUp.bind(this)); } } onMouseMove(event) { if (!this.enabled) return; event.preventDefault(); const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; if (this.state === 0) { this.sphericalDelta.theta -= movementX * 0.01; this.sphericalDelta.phi -= movementY * 0.01; } else if (this.state === 2) { this.panOffset.x -= movementX * 0.01; this.panOffset.y += movementY * 0.01; } } onMouseUp() { this.state = -1; document.removeEventListener('mousemove', this.onMouseMove); document.removeEventListener('mouseup', this.onMouseUp); } onMouseWheel(event) { if (!this.enabled) return; event.preventDefault(); if (event.deltaY < 0) { this.scale /= 1.1; } else if (event.deltaY > 0) { this.scale *= 1.1; } this.zoomChanged = true; } onDoubleClick() { this.target.set(0, 10, 0); this.spherical.set(1, Math.PI / 2, 0); this.spherical.radius = 45; this.sphericalDelta.set(0, 0, 0); this.panOffset.set(0, 0, 0); this.scale = 1; this.update(); } // Touch event handlers for mobile onTouchStart(event) { if (!this.enabled || event.touches.length !== 1) return; event.preventDefault(); const touch = event.touches[0]; lastTouchX = touch.clientX; lastTouchY = touch.clientY; touchStartTime = Date.now(); // Check if we're touching an orb (will be handled separately) const mouse = new THREE.Vector2( (touch.clientX / window.innerWidth) * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1 ); raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(boneOrbs); if (intersects.length > 0) { // Orb touch - let the orb handler deal with it return; } // Otherwise, start orbit camera isTouchOrbiting = true; this.state = 0; } onTouchMove(event) { if (!this.enabled || !isTouchOrbiting || event.touches.length !== 1) return; event.preventDefault(); const touch = event.touches[0]; const deltaX = touch.clientX - lastTouchX; const deltaY = touch.clientY - lastTouchY; if (this.state === 0) { this.sphericalDelta.theta -= deltaX * 0.01; this.sphericalDelta.phi -= deltaY * 0.01; } lastTouchX = touch.clientX; lastTouchY = touch.clientY; } onTouchEnd(event) { if (isTouchOrbiting) { isTouchOrbiting = false; this.state = -1; } } update() { const offset = new THREE.Vector3(); this.spherical.theta += this.sphericalDelta.theta; this.spherical.phi += this.sphericalDelta.phi; this.spherical.phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, this.spherical.phi)); if (this.zoomChanged) { this.spherical.radius *= this.scale; this.spherical.radius = Math.max(this.minDistance, Math.min(this.maxDistance, this.spherical.radius)); this.scale = 1; this.zoomChanged = false; } this.target.add(this.panOffset); this.panOffset.set(0, 0, 0); this.target.y = Math.max(0, Math.min(30, this.target.y)); offset.setFromSpherical(this.spherical); this.camera.position.copy(this.target).add(offset); this.camera.lookAt(this.target); this.sphericalDelta.set(0, 0, 0); } } init(); function init() { scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a1a2e); camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); camera.position.set(0, 15, 45); const ambient = new THREE.AmbientLight(0xffffff, 0.4); scene.add(ambient); const light = new THREE.DirectionalLight(0xffffff, 0.8); light.position.set(0, 20, 10); scene.add(light); const rimLight = new THREE.DirectionalLight(0x4a44b5, 0.3); rimLight.position.set(0, 10, -20); scene.add(rimLight); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const groundGeometry = new THREE.PlaneGeometry(50, 50); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x2a2a3a, roughness: 0.8, metalness: 0.2 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.position.y = -0.1; scene.add(ground); raycaster = new THREE.Raycaster(); mouse = new THREE.Vector2(); orbGroup = new THREE.Group(); scene.add(orbGroup); controls = new SimpleOrbitControls(camera, renderer.domElement); helper = new THREE.MMDAnimationHelper(); const loader = new THREE.MMDLoader(); function getPMXPath() { const params = new URLSearchParams(window.location.search); const pmxParam = params.get('pmx'); if (pmxParam) { // prepend folder path and decode URI safely return "pmx/pronama/" + decodeURIComponent(pmxParam); } return "pmx/pronama/YusakuFujiki/yusaku.pmx"; // default fallback } const modelPath = getPMXPath(); loader.load( modelPath, function (mesh) { model = mesh; model.position.y = 0; scene.add(model); document.getElementById('loading').style.display = 'none'; document.getElementById('ui').style.display = 'block'; // Show mobile UI toggle on mobile if (isMobile) { document.getElementById('mobile-ui-toggle').style.display = 'flex'; // Hide UI by default on mobile to maximize canvas space document.getElementById('ui').style.display = 'none'; } model.updateMatrixWorld(true); model.skeleton.bones.forEach(bone => { bone.userData.bindQuaternion = bone.quaternion.clone(); }); createAllOrbControls(); setupUI(); setupOrbInteraction(); setupAxisControls(); setupMobileControls(); animate(); }, function (xhr) { if (xhr.lengthComputable) { const percentComplete = (xhr.loaded / xhr.total * 100).toFixed(0); document.getElementById('loading-progress').textContent = `${percentComplete}%`; } }, function (error) { console.error("Error loading model:", error); document.getElementById('loading').innerHTML = ` <div style="color:#ff6b6b">Error loading model</div> <div style="margin-top:10px;font-size:14px;">Check browser console for details</div> <button onclick="location.reload()" style="margin-top:15px;padding:8px 15px;background:#6c63ff;border:none;border-radius:4px;color:white;cursor:pointer;">Retry</button> `; } ); window.addEventListener("resize", onWindowResize); } function setupMobileControls() { const orbitBtn = document.getElementById('mobile-orbit'); const zoomInBtn = document.getElementById('mobile-zoom-in'); const zoomOutBtn = document.getElementById('mobile-zoom-out'); const resetCamBtn = document.getElementById('mobile-reset-cam'); const uiToggle = document.getElementById('mobile-ui-toggle'); orbitBtn.addEventListener('click', function() { showNotification('Drag with one finger to orbit camera'); }); zoomInBtn.addEventListener('click', function() { controls.scale /= 1.2; controls.zoomChanged = true; }); zoomOutBtn.addEventListener('click', function() { controls.scale *= 1.2; controls.zoomChanged = true; }); resetCamBtn.addEventListener('click', function() { controls.onDoubleClick(); showNotification('Camera reset'); }); uiToggle.addEventListener('click', function() { const ui = document.getElementById('ui'); if (ui.style.display === 'none') { ui.style.display = 'block'; } else { ui.style.display = 'none'; } }); } function createAllOrbControls() { orbGroup.children = []; boneOrbs = []; if (!model || !model.skeleton) return; const bones = model.skeleton.bones; Object.keys(ESSENTIAL_BONES).forEach(japaneseName => { const bone = bones.find(b => b.name === japaneseName); if (bone) { createOrbForBone(bone, ESSENTIAL_BONES[japaneseName]); } }); console.log(`Created ${boneOrbs.length} orbs for essential bones`); } function createOrbForBone(bone, englishName) { const boneWorldPos = new THREE.Vector3(); bone.getWorldPosition(boneWorldPos); const orbGeometry = new THREE.SphereGeometry(0.8, 16, 16); let orbColor; if (englishName.includes('eye')) { orbColor = 0x6b6bff; } else if (englishName.includes('wrist') || englishName.includes('ankle')) { orbColor = 0x6bff6b; } else { orbColor = 0xff6b6b; } const orbMaterial = new THREE.MeshBasicMaterial({ color: orbColor, transparent: true, opacity: englishName.includes('eye') ? 0.5 : 0.9 }); const orb = new THREE.Mesh(orbGeometry, orbMaterial); orb.position.copy(boneWorldPos); orb.userData = { type: 'bone_control', bone: bone, boneName: englishName, originalPosition: boneWorldPos.clone() }; orbGroup.add(orb); boneOrbs.push(orb); } function setupOrbInteraction() { renderer.domElement.addEventListener('mousedown', onMouseDown); renderer.domElement.addEventListener('mousemove', onMouseMove); renderer.domElement.addEventListener('mouseup', onMouseUp); renderer.domElement.addEventListener('click', onOrbClick); // Touch events for mobile orb interaction renderer.domElement.addEventListener('touchstart', onTouchStartOrb, { passive: false }); renderer.domElement.addEventListener('touchmove', onTouchMoveOrb, { passive: false }); renderer.domElement.addEventListener('touchend', onTouchEndOrb); } function onTouchStartOrb(event) { if (event.touches.length !== 1) return; event.preventDefault(); const touch = event.touches[0]; mouse.x = (touch.clientX / window.innerWidth) * 2 - 1; mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(boneOrbs); if (intersects.length > 0) { isTouchDraggingOrb = true; touchOrb = intersects[0].object; touchOrbStartTime = Date.now(); lastTouchX = touch.clientX; lastTouchY = touch.clientY; // Show axis controls on long press (for mobile) setTimeout(() => { if (isTouchDraggingOrb && touchOrb && Date.now() - touchOrbStartTime > 500) { showAxisControls(touchOrb); } }, 500); } } function onTouchMoveOrb(event) { if (!isTouchDraggingOrb || !touchOrb || event.touches.length !== 1) return; event.preventDefault(); const touch = event.touches[0]; const deltaX = touch.clientX - lastTouchX; const deltaY = touch.clientY - lastTouchY; const bone = touchOrb.userData.bone; const boneName = touchOrb.userData.boneName; if (controlMode === 'rotation') { // Rotation mode if (boneName.includes('eye')) { bone.rotation.y += deltaX * 0.01; bone.rotation.x += deltaY * 0.01; } else { bone.rotation.y += deltaX * 0.02; bone.rotation.x += deltaY * 0.02; } bone.rotation.x = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.x)); bone.rotation.y = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.y)); bone.rotation.z = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.z)); } else { // Position mode if (boneName.includes('wrist') || boneName.includes('ankle') || boneName.includes('eye')) { bone.position.x += deltaX * 0.05; bone.position.y -= deltaY * 0.05; } } bone.updateMatrixWorld(true); updateOrbPositions(); lastTouchX = touch.clientX; lastTouchY = touch.clientY; } function onTouchEndOrb(event) { if (isTouchDraggingOrb) { // Check if it was a tap (not a drag) to show axis controls if (touchOrb && Date.now() - touchOrbStartTime < 300) { showAxisControls(touchOrb); } isTouchDraggingOrb = false; touchOrb = null; } } function onOrbClick(event) { if (event.button !== 0) return; mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(boneOrbs); if (intersects.length > 0) { const clickedOrb = intersects[0].object; showAxisControls(clickedOrb); event.stopPropagation(); } else { hideAxisControls(); } } function showAxisControls(orb) { selectedOrb = orb; const axisControls = document.getElementById('axis-controls'); axisControls.style.display = 'block'; const screenPosition = new THREE.Vector3(); orb.getWorldPosition(screenPosition); screenPosition.project(camera); const x = (screenPosition.x * 0.5 + 0.5) * window.innerWidth; const y = -(screenPosition.y * 0.5 - 0.5) * window.innerHeight; axisControls.style.left = x + 'px'; axisControls.style.top = y + 'px'; } function hideAxisControls() { selectedOrb = null; document.getElementById('axis-controls').style.display = 'none'; } function setupAxisControls() { 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)); // Add touch events for axis controls on mobile const axisButtons = document.querySelectorAll('.axis-btn'); axisButtons.forEach(btn => { btn.addEventListener('touchstart', function(e) { e.preventDefault(); this.click(); }, { passive: false }); }); } function moveBoneOnAxis(axis, direction) { if (!selectedOrb) return; const bone = selectedOrb.userData.bone; const boneName = selectedOrb.userData.boneName; const rotationStep = isMobile ? 0.05 : 0.1; const positionStep = isMobile ? 0.25 : 0.5; if (controlMode === 'rotation') { // Rotation mode if (boneName.includes('eye')) { bone.rotation[axis] += direction * rotationStep * 0.1; } else { bone.rotation[axis] += direction * rotationStep; } bone.rotation.x = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.x)); bone.rotation.y = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.y)); bone.rotation.z = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.z)); } else { // Position mode if (boneName.includes('wrist') || boneName.includes('ankle') || boneName.includes('eye')) { bone.position[axis] += direction * positionStep; } } bone.updateMatrixWorld(true); updateOrbPositions(); } function onMouseDown(event) { if (event.button !== 0) return; mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(boneOrbs); if (intersects.length > 0) { isDraggingOrb = true; currentOrb = intersects[0].object; renderer.domElement.classList.add('orb-dragging'); document.body.style.cursor = 'grabbing'; event.preventDefault(); event.stopPropagation(); } } function onMouseMove(event) { if (!isDraggingOrb || !currentOrb) return; const bone = currentOrb.userData.bone; const boneName = currentOrb.userData.boneName; const deltaX = event.movementX * 0.01; const deltaY = event.movementY * 0.01; if (controlMode === 'rotation') { // Rotation mode if (boneName.includes('eye')) { bone.rotation.y += deltaX * 0.1; bone.rotation.x += deltaY * 0.1; } else { bone.rotation.y += deltaX; bone.rotation.x += deltaY; } bone.rotation.x = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.x)); bone.rotation.y = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.y)); bone.rotation.z = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.z)); } else { // Position mode if (boneName.includes('wrist') || boneName.includes('ankle') || boneName.includes('eye')) { bone.position.x += deltaX * 2; bone.position.y -= deltaY * 2; } } bone.updateMatrixWorld(true); updateOrbPositions(); } function onMouseUp(event) { if (event.button === 0 && isDraggingOrb) { isDraggingOrb = false; currentOrb = null; renderer.domElement.classList.remove('orb-dragging'); document.body.style.cursor = 'default'; } } function updateOrbPositions() { boneOrbs.forEach(orb => { const bone = orb.userData.bone; const boneWorldPos = new THREE.Vector3(); bone.getWorldPosition(boneWorldPos); orb.position.copy(boneWorldPos); }); } function getCurrentPose() { if (!model || !model.skeleton) return {}; const pose = {}; const bones = model.skeleton.bones; model.updateMatrixWorld(true); Object.keys(ESSENTIAL_BONES).forEach(japaneseName => { const bone = bones.find(b => b.name === japaneseName); if (!bone) return; const bindQuat = bone.userData?.bindQuaternion ? bone.userData.bindQuaternion.clone() : new THREE.Quaternion(); // fallback identity // relative rotation: bind^-1 * current const invBind = bindQuat.clone(); if (typeof invBind.invert === "function") invBind.invert(); else invBind.inverse(); const relativeQuat = invBind.multiply(bone.quaternion).normalize(); pose[japaneseName] = { quaternion: { x: relativeQuat.x, y: relativeQuat.y, z: relativeQuat.z, w: relativeQuat.w } }; }); return pose; } // Apply a portable pose (rotation only) function applyPose(pose) { if (!model || !model.skeleton) return; const bones = model.skeleton.bones; model.updateMatrixWorld(true); Object.keys(pose).forEach(japaneseName => { const bone = 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 ); const bindQuat = bone.userData?.bindQuaternion ? bone.userData.bindQuaternion.clone() : new THREE.Quaternion(); bone.quaternion.copy(bindQuat.clone().multiply(relQuat).normalize()); bone.updateMatrixWorld(true); }); model.skeleton.update(); updateOrbPositions(); } function exportToVPD() { if (!model || !model.skeleton) return; const bones = model.skeleton.bones; let vpdContent = "Vocaloid Pose Data file\r\n\r\n0;\r\n\r\n"; const exportBones = []; // Update all matrices first model.updateMatrixWorld(true); Object.keys(ESSENTIAL_BONES).forEach(japaneseName => { const bone = bones.find(b => b.name === japaneseName); if (!bone) return; // Get current bone quaternion const currentQuat = bone.quaternion.clone(); // Get bind pose quaternion (store once on load) const bindQuat = bone.userData?.bindQuaternion ? bone.userData.bindQuaternion.clone() : new THREE.Quaternion(); // default identity if missing // Compute relative rotation (Three.js < r150 uses inverse()) const invBind = bindQuat.clone(); if (typeof invBind.invert === "function") { invBind.invert(); } else { invBind.inverse(); } const relativeQuat = invBind.multiply(currentQuat); relativeQuat.normalize(); const rotationThreshold = 0.0001; const hasRotation = Math.abs(relativeQuat.x) > rotationThreshold || Math.abs(relativeQuat.y) > rotationThreshold || Math.abs(relativeQuat.z) > rotationThreshold || Math.abs(relativeQuat.w - 1) > rotationThreshold; if (hasRotation) { exportBones.push({ name: japaneseName, quaternion: relativeQuat, index: exportBones.length }); } }); // Update bone count vpdContent = vpdContent.replace("0;\r\n\r\n", `${exportBones.length};\r\n\r\n`); exportBones.forEach(boneData => { vpdContent += `Bone${boneData.index}{${boneData.name}\r\n`; vpdContent += " 0.000000,0.000000,0.000000;\r\n"; // Convert to MMD quaternion (flip handedness only) const q = boneData.quaternion; const mmdQuat = new THREE.Quaternion(-q.x, q.z, q.y, -q.w); mmdQuat.normalize(); vpdContent += ` ${mmdQuat.x.toFixed(6)},${mmdQuat.y.toFixed(6)},${mmdQuat.z.toFixed(6)},${mmdQuat.w.toFixed(6)};\r\n`; vpdContent += "}\r\n"; }); // Save file const BOM = '\uFEFF'; const blob = new Blob([BOM + vpdContent], { type: 'text/plain; charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'pose.vpd'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification(`VPD exported! ${exportBones.length} modified bones.`); console.log("VPD Content:\n", vpdContent); console.log("Exported relative bone rotations:"); exportBones.forEach(boneData => { console.log(boneData.name, boneData.quaternion); }); } function setupUI() { const resetBtn = document.getElementById('reset-btn'); const exportBtn = document.getElementById('export-btn'); const rotationModeBtn = document.getElementById('rotation-mode'); const positionModeBtn = document.getElementById('position-mode'); const modeIndicator = document.getElementById('control-mode-indicator'); const stor1Btn = document.getElementById('stor1-btn'); const stor2Btn = document.getElementById('stor2-btn'); const load1Btn = document.getElementById('load1-btn'); const load2Btn = document.getElementById('load2-btn'); // Make buttons touch-friendly on mobile if (isMobile) { const buttons = document.querySelectorAll('button'); buttons.forEach(btn => { btn.addEventListener('touchstart', function(e) { e.preventDefault(); this.click(); }, { passive: false }); }); } resetBtn.addEventListener('click', function() { if (model && model.skeleton) { const bones = model.skeleton.bones; bones.forEach(bone => { bone.rotation.set(0, 0, 0); bone.position.set(0, 0, 0); }); model.skeleton.update(); updateOrbPositions(); hideAxisControls(); showNotification('Pose reset to default'); } }); exportBtn.addEventListener('click', exportToVPD); rotationModeBtn.addEventListener('click', function() { controlMode = 'rotation'; rotationModeBtn.classList.add('active'); positionModeBtn.classList.remove('active'); modeIndicator.textContent = 'Current Mode: Rotation'; }); positionModeBtn.addEventListener('click', function() { controlMode = 'position'; positionModeBtn.classList.add('active'); rotationModeBtn.classList.remove('active'); modeIndicator.textContent = 'Current Mode: Position'; }); stor1Btn.addEventListener('click', function() { const pose = getCurrentPose(); localStorage.setItem('stor1', JSON.stringify(pose)); showNotification('Pose saved to Slot 1'); }); stor2Btn.addEventListener('click', function() { const pose = getCurrentPose(); localStorage.setItem('stor2', JSON.stringify(pose)); showNotification('Pose saved to Slot 2'); }); load1Btn.addEventListener('click', function() { const storedPose = localStorage.getItem('stor1'); if (storedPose) { const pose = JSON.parse(storedPose); console.log('Loaded from Slot 1:', pose); applyPose(pose); showNotification('Pose loaded from Slot 1'); } else { console.log('No pose found in Slot 1'); showNotification('No pose found in Slot 1', 2000); } }); load2Btn.addEventListener('click', function() { const storedPose = localStorage.getItem('stor2'); if (storedPose) { const pose = JSON.parse(storedPose); console.log('Loaded from Slot 2:', pose); applyPose(pose); showNotification('Pose loaded from Slot 2'); } else { console.log('No pose found in Slot 2'); showNotification('No pose found in Slot 2', 2000); } }); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } function animate() { requestAnimationFrame(animate); controls.update(); if (!isDraggingOrb && !isTouchDraggingOrb) { updateOrbPositions(); } renderer.render(scene, camera); } </script> </body> </html>