mirror of
https://github.com/promptpirate-x/discord-id-bypass-tool.git
synced 2026-06-09 22:43:53 +02:00
1012 lines
49 KiB
HTML
1012 lines
49 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Discord ID Bypass Tool — by PromptPirate</title>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Space+Grotesk:wght@400;600&display=swap');
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{background:#0a0a0f;color:#e0e0e8;font-family:'JetBrains Mono',monospace;overflow:hidden;height:100vh}
|
||
canvas{display:block}
|
||
.hud{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:10}
|
||
.hp{position:absolute;background:rgba(10,10,20,0.85);backdrop-filter:blur(12px);border:1px solid rgba(100,220,255,0.15);border-radius:8px;padding:14px 18px;pointer-events:auto}
|
||
.ht{font-family:'Space Grotesk',sans-serif;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:2.5px;color:rgba(100,220,255,0.7);margin-bottom:10px;display:flex;align-items:center;gap:8px}
|
||
.ht::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:#64dcff;box-shadow:0 0 8px #64dcff}
|
||
.main-title{position:absolute;top:20px;left:20px;font-family:'Space Grotesk',sans-serif;font-size:18px;font-weight:600;letter-spacing:1px;color:#e8e8f0;text-shadow:0 0 20px rgba(100,220,255,0.3);pointer-events:none}
|
||
.main-title span{color:#64dcff}
|
||
.controller-status{top:20px;right:20px}
|
||
.sd{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;vertical-align:middle}
|
||
.sd.on{background:#4ade80;box-shadow:0 0 8px #4ade80}.sd.off{background:#f87171;box-shadow:0 0 8px #f87171}
|
||
.controls-panel{bottom:20px;left:20px;max-width:320px}
|
||
.cr{display:flex;justify-content:space-between;align-items:center;padding:4px 0;font-size:11px;color:#a0a0b0;gap:12px}
|
||
.ck{background:rgba(100,220,255,0.1);border:1px solid rgba(100,220,255,0.25);border-radius:4px;padding:2px 8px;font-size:10px;color:#64dcff;font-family:'JetBrains Mono',monospace;white-space:nowrap}
|
||
.values-panel{bottom:20px;right:20px;min-width:220px}
|
||
.vr{display:flex;justify-content:space-between;align-items:center;padding:3px 0;font-size:12px}
|
||
.vl{color:#808090}.vn{color:#64dcff;font-weight:600;font-variant-numeric:tabular-nums}
|
||
.vbt{width:80px;height:4px;background:rgba(100,220,255,0.1);border-radius:2px;overflow:hidden;margin-left:10px}
|
||
.vbf{height:100%;background:linear-gradient(90deg,#64dcff,#a78bfa);border-radius:2px;transition:width 0.05s linear}
|
||
.sensitivity-panel{top:20px;left:28%;transform:translateX(-50%)}
|
||
.sr{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:11px;color:#a0a0b0}
|
||
.sb{background:rgba(100,220,255,0.1);border:1px solid rgba(100,220,255,0.3);color:#64dcff;width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center}
|
||
.sb:hover{background:rgba(100,220,255,0.25)}
|
||
.sv{color:#64dcff;font-weight:600;min-width:30px;text-align:center}
|
||
.cb{flex:1;background:rgba(167,139,250,0.1);border:1px solid rgba(167,139,250,0.3);color:#a78bfa;padding:5px 8px;border-radius:4px;cursor:pointer;font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:0.3px;white-space:nowrap}
|
||
.cb:hover{background:rgba(167,139,250,0.2);border-color:rgba(167,139,250,0.5)}
|
||
.model-panel{top:80px;right:20px;max-width:380px;max-height:calc(100vh - 200px);overflow-y:auto}
|
||
.model-panel::-webkit-scrollbar{width:5px}.model-panel::-webkit-scrollbar-track{background:transparent}.model-panel::-webkit-scrollbar-thumb{background:rgba(100,220,255,0.2);border-radius:3px}
|
||
.lbtn{background:rgba(100,220,255,0.1);border:1px solid rgba(100,220,255,0.3);color:#64dcff;padding:8px 16px;border-radius:6px;cursor:pointer;font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.5px;width:100%}
|
||
.lbtn:hover{background:rgba(100,220,255,0.2);border-color:rgba(100,220,255,0.5)}
|
||
#model-file{display:none}
|
||
.li{font-size:10px;color:#707080;margin-top:8px;line-height:1.5;white-space:pre-line}
|
||
.err{background:rgba(248,113,113,0.1);border:1px solid rgba(248,113,113,0.3);border-radius:4px;padding:8px;margin-top:8px;font-size:10px;color:#f87171;display:none;word-break:break-all}
|
||
.ok-box{background:rgba(74,222,128,0.08);border:1px solid rgba(74,222,128,0.25);border-radius:4px;padding:8px;margin-top:8px;font-size:10px;color:#4ade80;display:none;white-space:pre-line;line-height:1.5}
|
||
.bsel{background:rgba(10,10,20,0.9);border:1px solid rgba(100,220,255,0.2);color:#e0e0e8;padding:4px 8px;border-radius:4px;font-size:10px;font-family:'JetBrains Mono',monospace;width:100%;margin-top:2px}
|
||
.bsel option{background:#1a1a2e}
|
||
.brow{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:11px}
|
||
.blbl{min-width:45px;font-weight:600}
|
||
.blbl.hd{color:#4ade80}.blbl.nk{color:#facc15}.blbl.jw{color:#f97316}
|
||
.jaw-row{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:10px;color:#a0a0b0}
|
||
.axb{background:rgba(249,115,22,0.1);border:1px solid rgba(249,115,22,0.25);color:#f97316;padding:2px 8px;border-radius:3px;cursor:pointer;font-size:10px;font-family:'JetBrains Mono',monospace}
|
||
.axb:hover{background:rgba(249,115,22,0.25)}.axb.active{background:rgba(249,115,22,0.3);border-color:#f97316;font-weight:600}
|
||
.sub{font-size:10px;color:#5a5a6a;margin-top:3px}
|
||
.orbit-hint{position:absolute;bottom:55px;left:50%;transform:translateX(-50%);font-size:10px;color:#353545;pointer-events:none}
|
||
.format-badges{display:flex;gap:4px;margin-top:6px;flex-wrap:wrap}
|
||
.fbadge{font-size:9px;padding:2px 7px;border-radius:3px;font-weight:600;letter-spacing:0.5px}
|
||
.fbadge.vrm{background:rgba(251,146,60,0.15);color:#fb923c;border:1px solid rgba(251,146,60,0.3)}
|
||
.fbadge.fbx{background:rgba(167,139,250,0.15);color:#a78bfa;border:1px solid rgba(167,139,250,0.3)}
|
||
.fbadge.glb{background:rgba(100,220,255,0.15);color:#64dcff;border:1px solid rgba(100,220,255,0.3)}
|
||
.fbadge.zip{background:rgba(250,204,21,0.15);color:#facc15;border:1px solid rgba(250,204,21,0.3)}
|
||
.drop-zone{border:2px dashed rgba(100,220,255,0.3);border-radius:6px;padding:12px;margin-top:8px;text-align:center;font-size:10px;color:#5a5a6a;transition:all 0.2s;cursor:pointer}
|
||
.drop-zone.over{border-color:#64dcff;background:rgba(100,220,255,0.05);color:#64dcff}
|
||
.morph-tester{margin-top:8px;padding:8px;background:rgba(249,115,22,0.05);border:1px solid rgba(249,115,22,0.15);border-radius:6px}
|
||
.morph-tester-row{display:flex;align-items:center;gap:6px;padding:2px 0;font-size:10px;color:#b0b0c0}
|
||
.morph-tester-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;padding:2px 4px;border-radius:3px}
|
||
.morph-tester-name:hover{background:rgba(249,115,22,0.1)}
|
||
.morph-tester-name.active{color:#f97316;background:rgba(249,115,22,0.15)}
|
||
.morph-tester-val{min-width:30px;text-align:right;color:#f97316;font-weight:600}
|
||
.morph-slider{width:60px;accent-color:#f97316}
|
||
.morph-map{font-size:8px;padding:2px 6px;border-radius:3px;background:rgba(74,222,128,0.08);color:#4ade80;border:1px solid rgba(74,222,128,0.2);cursor:pointer;white-space:nowrap;font-family:'JetBrains Mono',monospace}
|
||
.morph-map:hover{background:rgba(74,222,128,0.2);border-color:#4ade80}
|
||
.morph-map.mapped{background:rgba(74,222,128,0.2);border-color:#4ade80;font-weight:600}
|
||
.morph-auto{font-size:9px;padding:2px 6px;border-radius:3px;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.25)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="canvas-container" style="width:100vw;height:100vh;position:relative;">
|
||
<div class="hud">
|
||
<div class="main-title"><span>☠</span> Discord ID Bypass Tool <span style="font-size:11px;opacity:0.5;margin-left:6px">by PromptPirate</span></div>
|
||
|
||
<div class="hp controller-status">
|
||
<div class="ht">Input</div>
|
||
<div style="font-size:12px;color:#c0c0cc"><span class="sd off" id="gp-dot"></span><span id="gp-status">No Gamepad</span></div>
|
||
<div class="sub" id="gp-hint">Press any button on controller</div>
|
||
<div class="sub">Keyboard: Always Active</div>
|
||
</div>
|
||
|
||
<div class="hp sensitivity-panel">
|
||
<div class="ht">Sensitivity</div>
|
||
<div class="sr"><span style="min-width:45px">Head</span><button class="sb" id="hs-d">−</button><span class="sv" id="hs-v">1.0</span><button class="sb" id="hs-u">+</button></div>
|
||
<div class="sr"><span style="min-width:45px">Mouth</span><button class="sb" id="ms-d">−</button><span class="sv" id="ms-v">1.0</span><button class="sb" id="ms-u">+</button></div>
|
||
<div class="sr"><span style="min-width:45px;color:#f97316">Range</span><button class="sb" id="mr-d" style="border-color:rgba(249,115,22,0.3);color:#f97316">−</button><span class="sv" id="mr-v" style="color:#f97316">100%</span><button class="sb" id="mr-u" style="border-color:rgba(249,115,22,0.3);color:#f97316">+</button></div>
|
||
<div style="display:flex;gap:6px;margin-top:10px">
|
||
<button class="cb" id="cam-head">Focus Head</button>
|
||
<button class="cb" id="cam-body">Full Body</button>
|
||
<button class="cb" id="cam-reset">Reset Cam</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hp model-panel" id="model-panel">
|
||
<div class="ht">Model</div>
|
||
<button class="lbtn" id="load-btn">Load Model File</button>
|
||
<input type="file" id="model-file" accept=".vrm,.fbx,.glb,.gltf,.zip" />
|
||
<input type="file" id="model-multi" accept=".fbx,.png,.jpg,.jpeg,.tga,.bmp" multiple style="display:none" />
|
||
<div class="format-badges">
|
||
<span class="fbadge vrm">VRM</span>
|
||
<span class="fbadge fbx">FBX</span>
|
||
<span class="fbadge glb">GLB/GLTF</span>
|
||
<span class="fbadge zip">ZIP</span>
|
||
</div>
|
||
<div class="drop-zone" id="drop-zone">
|
||
Drop folder or ZIP here<br>for FBX + textures
|
||
</div>
|
||
<button class="lbtn" id="load-multi-btn" style="margin-top:4px;font-size:9px;padding:5px 10px;opacity:0.7">
|
||
Or select FBX + texture files together
|
||
</button>
|
||
<div class="li" id="model-info">VRM = instant setup. FBX with textures = use ZIP or drop folder. GLB = self-contained.</div>
|
||
<div class="err" id="err-box"></div>
|
||
<div class="ok-box" id="ok-box"></div>
|
||
|
||
<!-- Bone assignment dropdowns -->
|
||
<div id="bone-section" style="display:none;margin-top:12px">
|
||
<div class="ht">Bone Assignment</div>
|
||
<div class="brow"><span class="blbl hd">Head</span><select class="bsel" id="sel-head"><option value="">(none)</option></select></div>
|
||
<div class="brow"><span class="blbl nk">Neck</span><select class="bsel" id="sel-neck"><option value="">(none)</option></select></div>
|
||
<div class="brow"><span class="blbl jw">Jaw</span><select class="bsel" id="sel-jaw"><option value="">(none)</option></select></div>
|
||
|
||
<div id="jaw-section" style="display:none;margin-top:8px">
|
||
<div style="font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#f97316;opacity:0.7;margin-bottom:6px">Jaw Open Axis</div>
|
||
<div class="jaw-row">
|
||
<button class="axb active" data-a="x" id="ax-x">X</button>
|
||
<button class="axb" data-a="y" id="ax-y">Y</button>
|
||
<button class="axb" data-a="z" id="ax-z">Z</button>
|
||
<span style="margin-left:6px">Dir:</span>
|
||
<button class="axb active" data-d="1" id="dir-p">+</button>
|
||
<button class="axb" data-d="-1" id="dir-n">−</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="morph-section" style="display:none;margin-top:10px">
|
||
<div style="font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#f97316;opacity:0.7;margin-bottom:6px">Mouth (Morph Targets)</div>
|
||
<div style="font-size:9px;color:#6a6a7a;margin-bottom:6px">Mouth morph auto-assigned to RT/Space. Use tester below to preview each shape.</div>
|
||
<select class="bsel" id="morph-sel"><option value="">None (use jaw bone)</option></select>
|
||
<div class="morph-tester" id="morph-tester" style="display:none">
|
||
<div style="font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#f97316;opacity:0.7;margin-bottom:4px">Morph Tester — click name to assign to mouth</div>
|
||
<div id="morph-tester-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hp controls-panel">
|
||
<div class="ht">Controls</div>
|
||
<div class="cr"><span>Head Yaw</span><span class="ck">A D / L-Stick X</span></div>
|
||
<div class="cr"><span>Head Pitch</span><span class="ck">W S / L-Stick Y</span></div>
|
||
<div class="cr"><span>Head Roll</span><span class="ck">Q E / R-Stick X</span></div>
|
||
<div class="cr"><span>Mouth</span><span class="ck">Space / RT</span></div>
|
||
<div class="cr"><span>Reset</span><span class="ck">R / Y Button</span></div>
|
||
</div>
|
||
|
||
<div class="hp values-panel">
|
||
<div class="ht">State</div>
|
||
<div class="vr"><span class="vl">Yaw</span><span class="vn" id="v-yaw">0.0°</span></div>
|
||
<div class="vr"><span class="vl">Pitch</span><span class="vn" id="v-pitch">0.0°</span></div>
|
||
<div class="vr"><span class="vl">Roll</span><span class="vn" id="v-roll">0.0°</span></div>
|
||
<div class="vr"><span class="vl">Mouth</span><div style="display:flex;align-items:center"><span class="vn" id="v-mouth" style="margin-right:8px">0%</span><div class="vbt"><div class="vbf" id="mouth-bar" style="width:0%"></div></div></div></div>
|
||
</div>
|
||
<div class="orbit-hint">Right-click drag to orbit · Scroll to zoom</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||
<script>
|
||
// ═══════════════════════════════════════════════════════
|
||
// CONFIG & STATE
|
||
// ═══════════════════════════════════════════════════════
|
||
var CFG={hS:1,mS:1,mRange:1.0,yawMax:80,pitchMax:50,rollMax:35,dz:0.12,sm:0.12,msm:0.15,jawAxis:'x',jawDir:1,jawAngle:0.4};
|
||
var S={tY:0,tP:0,tR:0,tM:0,cY:0,cP:0,cR:0,cM:0,keys:{}};
|
||
var A={head:null,neck:null,jaw:null,headI:null,neckI:null,jawI:null,morphMesh:null,morphIdx:-1};
|
||
var loadedModel=null, vrmData=null, allBones=[];
|
||
var orbitTarget=new THREE.Vector3(0,0.3,0),orbitTheta=0,orbitPhi=0.15,orbitDist=3.5,isDrag=false,lastM={x:0,y:0};
|
||
|
||
function $(id){return document.getElementById(id)}
|
||
function showErr(m){$('err-box').textContent=m;$('err-box').style.display='block';$('ok-box').style.display='none'}
|
||
function showOk(m){$('ok-box').textContent=m;$('ok-box').style.display='block';$('err-box').style.display='none'}
|
||
function clearMsg(){$('err-box').style.display='none';$('ok-box').style.display='none'}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// THREE.JS SCENE
|
||
// ═══════════════════════════════════════════════════════
|
||
var container=$('canvas-container');
|
||
var scene=new THREE.Scene();scene.fog=new THREE.FogExp2(0x0a0a12,0.008);
|
||
var cam=new THREE.PerspectiveCamera(40,innerWidth/innerHeight,0.01,200);
|
||
cam.position.set(0,0.5,3.5);cam.lookAt(orbitTarget);
|
||
var ren=new THREE.WebGLRenderer({antialias:true});
|
||
ren.setSize(innerWidth,innerHeight);ren.setPixelRatio(Math.min(devicePixelRatio,2));
|
||
ren.setClearColor(0x0a0a12);ren.shadowMap.enabled=true;ren.shadowMap.type=THREE.PCFSoftShadowMap;
|
||
ren.toneMapping=THREE.ACESFilmicToneMapping;ren.toneMappingExposure=1.2;
|
||
container.prepend(ren.domElement);
|
||
|
||
scene.add(new THREE.AmbientLight(0x506080,0.8));
|
||
var kl=new THREE.DirectionalLight(0xdce8ff,1.4);kl.position.set(3,4,5);kl.castShadow=true;kl.shadow.mapSize.set(1024,1024);scene.add(kl);
|
||
var fl=new THREE.DirectionalLight(0x64dcff,0.4);fl.position.set(-3,2,2);scene.add(fl);
|
||
var rl=new THREE.DirectionalLight(0xa78bfa,0.5);rl.position.set(0,2,-4);scene.add(rl);
|
||
scene.add(new THREE.PointLight(0x64dcff,0.3,10).translateY(-2).translateZ(2));
|
||
var gnd=new THREE.Mesh(new THREE.PlaneGeometry(30,30),new THREE.MeshStandardMaterial({color:0x0a0a12,roughness:0.95}));
|
||
gnd.rotation.x=-Math.PI/2;gnd.position.y=-3;gnd.receiveShadow=true;scene.add(gnd);
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// ORBIT CAMERA
|
||
// ═══════════════════════════════════════════════════════
|
||
ren.domElement.addEventListener('mousedown',function(e){if(e.button===2||e.button===1||(e.button===0&&e.altKey)){isDrag=true;lastM={x:e.clientX,y:e.clientY};e.preventDefault()}});
|
||
window.addEventListener('mousemove',function(e){if(!isDrag)return;orbitTheta-=(e.clientX-lastM.x)*0.005;orbitPhi=Math.max(-1.2,Math.min(1.2,orbitPhi+(e.clientY-lastM.y)*0.005));lastM={x:e.clientX,y:e.clientY}});
|
||
window.addEventListener('mouseup',function(){isDrag=false});
|
||
ren.domElement.addEventListener('wheel',function(e){orbitDist=Math.max(0.3,Math.min(15,orbitDist+e.deltaY*0.005));e.preventDefault()},{passive:false});
|
||
ren.domElement.addEventListener('contextmenu',function(e){e.preventDefault()});
|
||
function updateCam(){
|
||
cam.position.x=orbitTarget.x+Math.sin(orbitTheta)*Math.cos(orbitPhi)*orbitDist;
|
||
cam.position.y=orbitTarget.y+Math.sin(orbitPhi)*orbitDist;
|
||
cam.position.z=orbitTarget.z+Math.cos(orbitTheta)*Math.cos(orbitPhi)*orbitDist;
|
||
cam.lookAt(orbitTarget);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// LOADER CHAIN: load all format loaders from CDN
|
||
// ═══════════════════════════════════════════════════════
|
||
var loadersReady={gltf:false,fbx:false,vrm:false};
|
||
var loaders={};
|
||
|
||
function loadScript(url,cb){
|
||
var s=document.createElement('script');
|
||
s.src=url;
|
||
s.onload=function(){console.log('[HC] Loaded:',url);cb()};
|
||
s.onerror=function(){console.warn('[HC] Failed:',url);cb()};
|
||
document.head.appendChild(s);
|
||
}
|
||
|
||
// Chain: GLTFLoader → fflate → FBXLoader → three-vrm → JSZip
|
||
loadScript('https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js',function(){
|
||
loadersReady.gltf=!!THREE.GLTFLoader;
|
||
if(THREE.GLTFLoader) loaders.gltf=new THREE.GLTFLoader();
|
||
|
||
loadScript('https://cdn.jsdelivr.net/npm/fflate@0.6.9/umd/index.js',function(){
|
||
loadScript('https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/FBXLoader.js',function(){
|
||
loadersReady.fbx=!!THREE.FBXLoader;
|
||
if(THREE.FBXLoader) loaders.fbx=new THREE.FBXLoader();
|
||
|
||
loadScript('https://cdn.jsdelivr.net/npm/@pixiv/three-vrm@0.6.11/lib/three-vrm.js',function(){
|
||
loadersReady.vrm=!!(window.THREE_VRM||THREE.VRM);
|
||
|
||
loadScript('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js',function(){
|
||
console.log('[HC] Loaders ready — GLTF:',loadersReady.gltf,'FBX:',loadersReady.fbx,'VRM:',loadersReady.vrm,'JSZip:',!!window.JSZip);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// MODEL LOADING — single file, ZIP, drop, multi-file
|
||
// ═══════════════════════════════════════════════════════
|
||
$('load-btn').onclick=function(){$('model-file').click()};
|
||
$('load-multi-btn').onclick=function(){$('model-multi').click()};
|
||
|
||
// Single file (VRM, FBX, GLB, ZIP)
|
||
$('model-file').onchange=function(e){
|
||
var f=e.target.files[0];if(!f)return;
|
||
handleFile(f);
|
||
};
|
||
|
||
// Multi-file (FBX + textures)
|
||
$('model-multi').onchange=function(e){
|
||
var files=Array.from(e.target.files);if(!files.length)return;
|
||
handleMultiFiles(files);
|
||
};
|
||
|
||
// Drop zone
|
||
var dzEl=$('drop-zone');
|
||
dzEl.addEventListener('dragover',function(e){e.preventDefault();e.stopPropagation();dzEl.classList.add('over')});
|
||
dzEl.addEventListener('dragleave',function(e){e.preventDefault();dzEl.classList.remove('over')});
|
||
dzEl.addEventListener('drop',function(e){
|
||
e.preventDefault();e.stopPropagation();dzEl.classList.remove('over');
|
||
var items=e.dataTransfer.items;
|
||
if(items&&items.length){
|
||
// Check for folder (webkitGetAsEntry)
|
||
var entries=[];
|
||
for(var i=0;i<items.length;i++){
|
||
var entry=items[i].webkitGetAsEntry&&items[i].webkitGetAsEntry();
|
||
if(entry) entries.push(entry);
|
||
}
|
||
if(entries.length===1&&entries[0].isDirectory){
|
||
readDirectoryEntries(entries[0]);
|
||
return;
|
||
}
|
||
}
|
||
// Fallback: treat as file list
|
||
var files=Array.from(e.dataTransfer.files);
|
||
if(files.length===1) handleFile(files[0]);
|
||
else if(files.length>1) handleMultiFiles(files);
|
||
});
|
||
|
||
// Also allow clicking drop zone
|
||
dzEl.addEventListener('click',function(){$('model-file').click()});
|
||
|
||
function readDirectoryEntries(dirEntry){
|
||
clearMsg();$('model-info').textContent='Reading folder...';
|
||
var reader=dirEntry.createReader();
|
||
var allEntries=[];
|
||
function readBatch(){
|
||
reader.readEntries(function(entries){
|
||
if(entries.length===0){
|
||
processDirectoryFiles(allEntries);
|
||
} else {
|
||
allEntries=allEntries.concat(entries);
|
||
readBatch();
|
||
}
|
||
});
|
||
}
|
||
readBatch();
|
||
}
|
||
|
||
function processDirectoryFiles(entries){
|
||
var filePromises=[];
|
||
for(var i=0;i<entries.length;i++){
|
||
if(entries[i].isFile){
|
||
filePromises.push(new Promise(function(resolve){
|
||
entries[i].file(function(f){resolve(f)});
|
||
}));
|
||
}
|
||
}
|
||
Promise.all(filePromises).then(function(files){handleMultiFiles(files)});
|
||
}
|
||
|
||
function handleFile(f){
|
||
clearMsg();
|
||
var ext=f.name.split('.').pop().toLowerCase();
|
||
$('model-info').textContent='Loading '+f.name+'...';
|
||
|
||
if(ext==='zip'){
|
||
loadZip(f);
|
||
} else {
|
||
var url=URL.createObjectURL(f);
|
||
if(ext==='vrm') loadVRM(url,f.name);
|
||
else if(ext==='fbx') loadFBX(url,f.name);
|
||
else loadGLB(url,f.name);
|
||
}
|
||
}
|
||
|
||
function handleMultiFiles(files){
|
||
clearMsg();
|
||
// Find the FBX/VRM/GLB
|
||
var modelFile=null;
|
||
var textureFiles=[];
|
||
for(var i=0;i<files.length;i++){
|
||
var ext=files[i].name.split('.').pop().toLowerCase();
|
||
if(['fbx','vrm','glb','gltf'].indexOf(ext)>=0&&!modelFile) modelFile=files[i];
|
||
else if(['png','jpg','jpeg','tga','bmp','tiff','webp'].indexOf(ext)>=0) textureFiles.push(files[i]);
|
||
}
|
||
if(!modelFile){showErr('No model file found in selection');return}
|
||
|
||
$('model-info').textContent='Loading '+modelFile.name+' + '+textureFiles.length+' textures...';
|
||
|
||
var ext=modelFile.name.split('.').pop().toLowerCase();
|
||
if(ext==='fbx'&&textureFiles.length>0){
|
||
loadFBXWithTextures(modelFile,textureFiles);
|
||
} else {
|
||
handleFile(modelFile);
|
||
}
|
||
}
|
||
|
||
// ─── ZIP LOADING ───
|
||
function loadZip(zipFile){
|
||
if(!window.JSZip){showErr('JSZip not loaded yet. Try again in a moment.');return}
|
||
$('model-info').textContent='Extracting '+zipFile.name+'...';
|
||
|
||
JSZip.loadAsync(zipFile).then(function(zip){
|
||
var modelEntry=null;
|
||
var textureBlobs={};
|
||
var promises=[];
|
||
|
||
zip.forEach(function(path,entry){
|
||
if(entry.dir) return;
|
||
var fn=path.split('/').pop().toLowerCase();
|
||
var ext=fn.split('.').pop();
|
||
|
||
if(['fbx','vrm','glb','gltf'].indexOf(ext)>=0&&!modelEntry){
|
||
modelEntry={path:path,ext:ext,entry:entry};
|
||
}
|
||
if(['png','jpg','jpeg','tga','bmp','webp'].indexOf(ext)>=0){
|
||
promises.push(entry.async('blob').then(function(blob){
|
||
var baseName=path.split('/').pop();
|
||
textureBlobs[baseName]=URL.createObjectURL(blob);
|
||
// Also store without extension variations
|
||
textureBlobs[baseName.toLowerCase()]=URL.createObjectURL(blob);
|
||
}));
|
||
}
|
||
});
|
||
|
||
if(!modelEntry){showErr('No model file found in ZIP');return}
|
||
|
||
Promise.all(promises).then(function(){
|
||
modelEntry.entry.async('arraybuffer').then(function(buf){
|
||
$('model-info').textContent='Loading '+modelEntry.path.split('/').pop()+'...';
|
||
|
||
if(modelEntry.ext==='fbx'){
|
||
loadFBXFromBuffer(buf,modelEntry.path.split('/').pop(),textureBlobs);
|
||
} else if(modelEntry.ext==='vrm'||modelEntry.ext==='glb'||modelEntry.ext==='gltf'){
|
||
var blob=new Blob([buf]);
|
||
var url=URL.createObjectURL(blob);
|
||
if(modelEntry.ext==='vrm') loadVRM(url,modelEntry.path.split('/').pop());
|
||
else loadGLB(url,modelEntry.path.split('/').pop());
|
||
}
|
||
});
|
||
});
|
||
}).catch(function(err){showErr('ZIP error: '+err.message)});
|
||
}
|
||
|
||
// ─── FBX WITH TEXTURES (multi-file or ZIP) ───
|
||
function loadFBXWithTextures(modelFile,textureFiles){
|
||
if(!loaders.fbx){showErr('FBXLoader not available');return}
|
||
|
||
// Create blob URLs for textures
|
||
var texMap={};
|
||
for(var i=0;i<textureFiles.length;i++){
|
||
var tf=textureFiles[i];
|
||
texMap[tf.name]=URL.createObjectURL(tf);
|
||
texMap[tf.name.toLowerCase()]=URL.createObjectURL(tf);
|
||
}
|
||
|
||
var reader=new FileReader();
|
||
reader.onload=function(){loadFBXFromBuffer(reader.result,modelFile.name,texMap)};
|
||
reader.readAsArrayBuffer(modelFile);
|
||
}
|
||
|
||
function loadFBXFromBuffer(buf,name,texMap){
|
||
if(!loaders.fbx){showErr('FBXLoader not available');return}
|
||
|
||
// Custom resource manager to resolve textures
|
||
var mgr=new THREE.LoadingManager();
|
||
mgr.setURLModifier(function(url){
|
||
// Extract filename from the path
|
||
var fn=url.split('/').pop().split('\\').pop();
|
||
if(texMap[fn]) return texMap[fn];
|
||
if(texMap[fn.toLowerCase()]) return texMap[fn.toLowerCase()];
|
||
// Try without path
|
||
var noExt=fn.replace(/\.[^.]+$/,'');
|
||
for(var key in texMap){
|
||
if(key.replace(/\.[^.]+$/,'').toLowerCase()===noExt.toLowerCase()) return texMap[key];
|
||
}
|
||
return url;
|
||
});
|
||
|
||
var fbxLoader=new THREE.FBXLoader(mgr);
|
||
try{
|
||
var obj=fbxLoader.parse(buf);
|
||
if(!prepareScene(obj,name)) return;
|
||
autoDetectByName();
|
||
}catch(err){
|
||
showErr('FBX parse error: '+err.message);
|
||
}
|
||
}
|
||
|
||
// ─── FBX (single file, no textures) ───
|
||
function loadFBX(url,name){
|
||
if(!loaders.fbx){showErr('FBXLoader not available. Try GLB format instead.');return}
|
||
loaders.fbx.load(url,function(obj){
|
||
URL.revokeObjectURL(url);
|
||
if(!prepareScene(obj,name)) return;
|
||
autoDetectByName();
|
||
},
|
||
function(p){if(p.total)$('model-info').textContent='Loading '+name+'... '+((p.loaded/p.total)*100|0)+'%'},
|
||
function(er){showErr('FBX load failed: '+(er.message||er));URL.revokeObjectURL(url)});
|
||
}
|
||
|
||
function prepareScene(obj,fileName){
|
||
if(loadedModel) scene.remove(loadedModel);
|
||
A.head=A.neck=A.jaw=null;A.headI=A.neckI=A.jawI=null;
|
||
A.morphMesh=null;A.morphIdx=-1;allBones=[];vrmData=null;
|
||
|
||
loadedModel=obj;
|
||
|
||
// Scale & center
|
||
var box=new THREE.Box3().setFromObject(loadedModel);
|
||
var sz=box.getSize(new THREE.Vector3()),ctr=box.getCenter(new THREE.Vector3());
|
||
var mx=Math.max(sz.x,sz.y,sz.z);
|
||
if(mx===0){showErr('Empty model');return false}
|
||
var sc=2.5/mx;
|
||
loadedModel.scale.multiplyScalar(sc);
|
||
loadedModel.position.sub(ctr.multiplyScalar(sc));
|
||
|
||
var nb=new THREE.Box3().setFromObject(loadedModel);
|
||
nb.getCenter(orbitTarget);
|
||
|
||
loadedModel.traverse(function(c){c.castShadow=true;c.receiveShadow=true});
|
||
scene.add(loadedModel);
|
||
|
||
// Collect ALL bones
|
||
loadedModel.traverse(function(c){if(c.isBone) allBones.push(c)});
|
||
|
||
// Collect ALL morph targets across all meshes
|
||
allMorphs=[];
|
||
loadedModel.traverse(function(c){
|
||
if(c.isMesh&&c.morphTargetDictionary){
|
||
var ent=Object.entries(c.morphTargetDictionary);
|
||
for(var i=0;i<ent.length;i++) allMorphs.push({mesh:c,name:ent[i][0],idx:ent[i][1],meshName:c.name});
|
||
}
|
||
});
|
||
|
||
// Populate morph dropdown + tester
|
||
var mSel=$('morph-sel');
|
||
mSel.innerHTML='<option value="">None (use jaw bone)</option>';
|
||
var testerDiv=$('morph-tester-list');
|
||
testerDiv.innerHTML='';
|
||
var autoMorph=null;
|
||
|
||
if(allMorphs.length){
|
||
$('morph-section').style.display='block';
|
||
$('morph-tester').style.display='block';
|
||
|
||
// Mouth-related patterns for auto-detection (priority order)
|
||
var mouthPatterns=[
|
||
/^mouth[_\s]?open$/i, /^jaw[_\s]?open$/i, /^aa?$/i,
|
||
/viseme.*aa/i, /viseme.*oh/i,
|
||
/mouth.*open/i, /jaw.*open/i, /open.*mouth/i,
|
||
/mouth.*a$/i, /^a$/i, /^oh$/i,
|
||
/mouth/i, /jaw/i
|
||
];
|
||
|
||
// Mesh name scoring: prefer body/face meshes, penalize eye/hair/accessory meshes
|
||
function meshScore(meshName){
|
||
var n=meshName.toLowerCase();
|
||
if(/eye|lash|brow|pupil/i.test(n)) return -10; // definitely wrong mesh
|
||
if(/body|face|head|skin|mesh/i.test(n)) return 5; // likely correct
|
||
if(/teeth|tongue|mouth/i.test(n)) return 3; // plausible
|
||
return 0; // neutral
|
||
}
|
||
|
||
var bestScore=-999, bestMorph=null, bestVal=null;
|
||
|
||
for(var i=0;i<allMorphs.length;i++){
|
||
var m=allMorphs[i];
|
||
var ov=JSON.stringify({mn:m.meshName,idx:m.idx,mi:i});
|
||
|
||
// Dropdown option
|
||
var o=document.createElement('option');
|
||
o.value=ov;
|
||
o.textContent=m.meshName+' → '+m.name;
|
||
mSel.appendChild(o);
|
||
|
||
// Score this morph for mouth auto-assignment
|
||
for(var p=0;p<mouthPatterns.length;p++){
|
||
if(mouthPatterns[p].test(m.name)){
|
||
// Higher pattern priority (lower index) = higher score
|
||
var score=(mouthPatterns.length-p)*10 + meshScore(m.meshName);
|
||
if(score>bestScore){
|
||
bestScore=score;bestMorph={morph:m,val:ov};
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Tester row
|
||
buildMorphTesterRow(testerDiv,m,i,ov);
|
||
}
|
||
|
||
// Auto-assign best mouth morph
|
||
if(bestMorph){
|
||
autoMorph=bestMorph;
|
||
mSel.value=autoMorph.val;
|
||
A.morphMesh=autoMorph.morph.mesh;
|
||
A.morphIdx=autoMorph.morph.idx;
|
||
var activeRow=testerDiv.querySelector('[data-mi="'+allMorphs.indexOf(autoMorph.morph)+'"]');
|
||
if(activeRow){
|
||
var n=activeRow.querySelector('.morph-tester-name');if(n)n.classList.add('active');
|
||
var b=activeRow.querySelector('.morph-map');if(b){b.classList.add('mapped');b.textContent='✓ Mapped'}
|
||
}
|
||
}
|
||
} else {
|
||
$('morph-section').style.display='none';
|
||
$('morph-tester').style.display='none';
|
||
}
|
||
|
||
$('model-info').textContent=fileName+' — '+allBones.length+' bones, '+allMorphs.length+' morphs';
|
||
return true;
|
||
}
|
||
|
||
var allMorphs=[];
|
||
|
||
function buildMorphTesterRow(container,morph,idx,optVal){
|
||
var row=document.createElement('div');
|
||
row.className='morph-tester-row';
|
||
row.setAttribute('data-mi',idx);
|
||
|
||
var name=document.createElement('span');
|
||
name.className='morph-tester-name';
|
||
name.textContent=morph.meshName+' → '+morph.name;
|
||
name.title='Click to assign as mouth control';
|
||
|
||
var val=document.createElement('span');
|
||
val.className='morph-tester-val';
|
||
val.textContent='0%';
|
||
|
||
var slider=document.createElement('input');
|
||
slider.type='range';slider.min='0';slider.max='300';slider.value='0';
|
||
slider.className='morph-slider';
|
||
|
||
var mapBtn=document.createElement('button');
|
||
mapBtn.className='morph-map';
|
||
mapBtn.textContent='▶ Map';
|
||
mapBtn.title='Map this morph to RT / Space';
|
||
|
||
// Slider: preview this morph in real-time
|
||
slider.oninput=function(){
|
||
var v=parseInt(this.value)/100;
|
||
morph.mesh.morphTargetInfluences[morph.idx]=v;
|
||
val.textContent=Math.round(v*100)+'%';
|
||
};
|
||
// Reset on release (unless it's the active mouth morph)
|
||
slider.onchange=function(){
|
||
if(A.morphMesh!==morph.mesh||A.morphIdx!==morph.idx){
|
||
morph.mesh.morphTargetInfluences[morph.idx]=0;
|
||
val.textContent='0%';
|
||
this.value='0';
|
||
}
|
||
};
|
||
|
||
function assignMorph(){
|
||
// Clear all active highlights and mapped states
|
||
container.querySelectorAll('.morph-tester-name').forEach(function(n){n.classList.remove('active')});
|
||
container.querySelectorAll('.morph-map').forEach(function(b){b.classList.remove('mapped');b.textContent='▶ Map'});
|
||
name.classList.add('active');
|
||
mapBtn.classList.add('mapped');
|
||
mapBtn.textContent='✓ Mapped';
|
||
A.morphMesh=morph.mesh;A.morphIdx=morph.idx;
|
||
$('morph-sel').value=optVal;
|
||
|
||
// Auto-set Range to match current slider preview value (if > 100%)
|
||
var sliderVal=parseInt(slider.value)/100;
|
||
if(sliderVal>1){
|
||
CFG.mRange=sliderVal;
|
||
$('mr-v').textContent=Math.round(CFG.mRange*100)+'%';
|
||
}
|
||
|
||
// Reset all other morph previews
|
||
for(var j=0;j<allMorphs.length;j++){
|
||
if(j!==idx) allMorphs[j].mesh.morphTargetInfluences[allMorphs[j].idx]=0;
|
||
}
|
||
var allSliders=container.querySelectorAll('.morph-slider');
|
||
allSliders.forEach(function(s,si){if(si!==idx){s.value='0'}});
|
||
var allVals=container.querySelectorAll('.morph-tester-val');
|
||
allVals.forEach(function(v,vi){if(vi!==idx){v.textContent='0%'}});
|
||
}
|
||
|
||
// Both name click and button click assign
|
||
name.onclick=assignMorph;
|
||
mapBtn.onclick=assignMorph;
|
||
|
||
row.appendChild(name);row.appendChild(val);row.appendChild(slider);row.appendChild(mapBtn);
|
||
container.appendChild(row);
|
||
}
|
||
|
||
// ─── VRM ───
|
||
function loadVRM(url,name){
|
||
if(!loaders.gltf){showErr('GLTFLoader not available');return}
|
||
|
||
loaders.gltf.load(url,function(gltf){
|
||
URL.revokeObjectURL(url);
|
||
|
||
// Try three-vrm parsing
|
||
var VRM=window.THREE_VRM||THREE.VRM||(typeof THREEVRM!=='undefined'?THREEVRM:null);
|
||
if(VRM&&VRM.from){
|
||
VRM.from(gltf).then(function(vrm){
|
||
if(!prepareScene(vrm.scene||gltf.scene,name)) return;
|
||
vrmData=vrm;
|
||
autoDetectVRM(vrm);
|
||
}).catch(function(err){
|
||
console.warn('[HC] VRM parse failed, falling back to GLTF bone detection:',err);
|
||
if(!prepareScene(gltf.scene,name)) return;
|
||
autoDetectByName();
|
||
});
|
||
} else {
|
||
// No VRM lib — parse as GLTF and detect bones by name
|
||
console.warn('[HC] three-vrm not loaded, using bone name detection');
|
||
if(!prepareScene(gltf.scene,name)) return;
|
||
autoDetectByName();
|
||
}
|
||
},
|
||
function(p){if(p.total)$('model-info').textContent='Loading '+name+'... '+((p.loaded/p.total)*100|0)+'%'},
|
||
function(er){showErr('VRM load failed: '+(er.message||er));URL.revokeObjectURL(url)});
|
||
}
|
||
|
||
// ─── GLB/GLTF ───
|
||
function loadGLB(url,name){
|
||
if(!loaders.gltf){showErr('GLTFLoader not available');return}
|
||
|
||
loaders.gltf.load(url,function(gltf){
|
||
URL.revokeObjectURL(url);
|
||
if(!prepareScene(gltf.scene,name)) return;
|
||
autoDetectByName();
|
||
},
|
||
function(p){if(p.total)$('model-info').textContent='Loading '+name+'... '+((p.loaded/p.total)*100|0)+'%'},
|
||
function(er){showErr('GLB load failed: '+(er.message||er));URL.revokeObjectURL(url)});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// BONE AUTO-DETECTION
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
// VRM: use standardized humanoid bone API
|
||
function autoDetectVRM(vrm){
|
||
var humanoid=vrm.humanoid;
|
||
if(!humanoid){autoDetectByName();return}
|
||
|
||
// VRM v0 API: getBoneNode(boneName)
|
||
var headBone=null,neckBone=null,jawBone=null;
|
||
try{
|
||
// Try different VRM schema approaches
|
||
var schema=THREE.VRMSchema||window.VRMSchema||(typeof THREEVRM!=='undefined'?THREEVRM.VRMSchema:null);
|
||
if(schema&&schema.HumanoidBoneName){
|
||
headBone=humanoid.getBoneNode(schema.HumanoidBoneName.Head);
|
||
neckBone=humanoid.getBoneNode(schema.HumanoidBoneName.Neck);
|
||
jawBone=humanoid.getBoneNode(schema.HumanoidBoneName.Jaw);
|
||
} else {
|
||
headBone=humanoid.getBoneNode('head');
|
||
neckBone=humanoid.getBoneNode('neck');
|
||
jawBone=humanoid.getBoneNode('jaw');
|
||
}
|
||
}catch(e){
|
||
console.warn('[HC] VRM humanoid API error:',e);
|
||
}
|
||
|
||
// If VRM API didn't find them, fall back to name matching
|
||
if(!headBone) headBone=matchBoneByName(['head']);
|
||
if(!neckBone) neckBone=matchBoneByName(['neck']);
|
||
if(!jawBone) jawBone=matchBoneByName(['jaw','chin']);
|
||
|
||
applyBoneAssignment(headBone,neckBone,jawBone,'VRM humanoid');
|
||
}
|
||
|
||
// Generic: match bones by name patterns (works for FBX, GLB, Mixamo, ReadyPlayerMe, etc.)
|
||
function autoDetectByName(){
|
||
var headBone=matchBoneByName(['head']);
|
||
var neckBone=matchBoneByName(['neck']);
|
||
var jawBone=matchBoneByName(['jaw','chin']);
|
||
|
||
applyBoneAssignment(headBone,neckBone,jawBone,'name matching');
|
||
}
|
||
|
||
function matchBoneByName(patterns){
|
||
// Pass 1: strict segment match (head in "mixamorig:Head", "head_06", "J_Bip_C_Head")
|
||
for(var p=0;p<patterns.length;p++){
|
||
var pat=patterns[p].toLowerCase();
|
||
for(var i=0;i<allBones.length;i++){
|
||
var n=allBones[i].name.toLowerCase();
|
||
if(n===pat) return allBones[i];
|
||
// Match as segment: surrounded by non-alpha or at boundaries
|
||
var re=new RegExp('(^|[^a-z])'+pat+'([^a-z]|$)');
|
||
if(re.test(n) && !/top|end|tip|nub|_ee?$/i.test(n)) return allBones[i];
|
||
}
|
||
}
|
||
// Pass 2: looser contains (but still exclude end-bones)
|
||
for(var p=0;p<patterns.length;p++){
|
||
var pat=patterns[p].toLowerCase();
|
||
for(var i=0;i<allBones.length;i++){
|
||
var n=allBones[i].name.toLowerCase();
|
||
if(n.indexOf(pat)>=0 && !/top|end|tip|nub|_ee?$/i.test(n)) return allBones[i];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function applyBoneAssignment(headBone,neckBone,jawBone,method){
|
||
if(headBone){A.head=headBone;A.headI=headBone.rotation.clone()}
|
||
if(neckBone){A.neck=neckBone;A.neckI=neckBone.rotation.clone()}
|
||
if(jawBone){A.jaw=jawBone;A.jawI=jawBone.rotation.clone();$('jaw-section').style.display='block'}
|
||
else{$('jaw-section').style.display='none'}
|
||
|
||
// Populate dropdowns
|
||
populateSelects(headBone,neckBone,jawBone);
|
||
$('bone-section').style.display='block';
|
||
|
||
// Status message
|
||
var msg='✓ Auto-detected via '+method+':\n';
|
||
msg+='Head: '+(headBone?headBone.name:'⚠ not found')+'\n';
|
||
msg+='Neck: '+(neckBone?neckBone.name:'⚠ not found')+'\n';
|
||
msg+='Jaw: '+(jawBone?jawBone.name:'not found');
|
||
if(A.morphMesh) msg+='\nMouth morph: auto-assigned ✓';
|
||
else if(allMorphs.length>0) msg+='\n'+allMorphs.length+' morphs found — use tester to pick mouth shape';
|
||
else if(!jawBone) msg+='\n⚠ No jaw bone or morphs. Head rotation only.';
|
||
if(!headBone) msg+='\n\n⚠ No head bone found. Select one manually from the dropdowns.';
|
||
showOk(msg);
|
||
|
||
console.log('[HC] Bones via',method,'— head:',headBone?.name,'neck:',neckBone?.name,'jaw:',jawBone?.name,'total:',allBones.length);
|
||
}
|
||
|
||
function populateSelects(headBone,neckBone,jawBone){
|
||
var roles=[{id:'sel-head',bone:headBone,key:'head'},{id:'sel-neck',bone:neckBone,key:'neck'},{id:'sel-jaw',bone:jawBone,key:'jaw'}];
|
||
for(var r=0;r<roles.length;r++){
|
||
var sel=$(roles[r].id);
|
||
sel.innerHTML='<option value="">(none)</option>';
|
||
for(var i=0;i<allBones.length;i++){
|
||
var o=document.createElement('option');
|
||
o.value=i;
|
||
o.textContent=allBones[i].name;
|
||
if(allBones[i]===roles[r].bone) o.selected=true;
|
||
sel.appendChild(o);
|
||
}
|
||
// Bind change handler
|
||
(function(key){
|
||
sel.onchange=function(){
|
||
// Reset old bone
|
||
if(A[key]&&A[key+'I']) A[key].rotation.copy(A[key+'I']);
|
||
if(this.value===''){A[key]=null;A[key+'I']=null}
|
||
else{
|
||
var bone=allBones[parseInt(this.value)];
|
||
A[key]=bone;A[key+'I']=bone.rotation.clone();
|
||
}
|
||
if(key==='jaw') $('jaw-section').style.display=A.jaw?'block':'none';
|
||
};
|
||
})(roles[r].key);
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// MORPH TARGET SELECTOR
|
||
// ═══════════════════════════════════════════════════════
|
||
$('morph-sel').onchange=function(){
|
||
// Reset all morph previews
|
||
for(var j=0;j<allMorphs.length;j++) allMorphs[j].mesh.morphTargetInfluences[allMorphs[j].idx]=0;
|
||
var testerDiv=$('morph-tester-list');
|
||
testerDiv.querySelectorAll('.morph-tester-name').forEach(function(n){n.classList.remove('active')});
|
||
testerDiv.querySelectorAll('.morph-slider').forEach(function(s){s.value='0'});
|
||
testerDiv.querySelectorAll('.morph-tester-val').forEach(function(v){v.textContent='0%'});
|
||
|
||
if(!this.value){A.morphMesh=null;A.morphIdx=-1;return}
|
||
var p=JSON.parse(this.value);
|
||
loadedModel.traverse(function(c){
|
||
if(c.isMesh&&c.name===p.mn){A.morphMesh=c;A.morphIdx=p.idx}
|
||
});
|
||
// Highlight matching tester row
|
||
if(p.mi!==undefined){
|
||
var row=testerDiv.querySelector('[data-mi="'+p.mi+'"] .morph-tester-name');
|
||
if(row) row.classList.add('active');
|
||
}
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// JAW AXIS CONFIG
|
||
// ═══════════════════════════════════════════════════════
|
||
$('ax-x').onclick=function(){setAx('x')};$('ax-y').onclick=function(){setAx('y')};$('ax-z').onclick=function(){setAx('z')};
|
||
$('dir-p').onclick=function(){setDir(1)};$('dir-n').onclick=function(){setDir(-1)};
|
||
function setAx(a){CFG.jawAxis=a;document.querySelectorAll('[data-a]').forEach(function(b){b.classList.toggle('active',b.getAttribute('data-a')===a)})}
|
||
function setDir(d){CFG.jawDir=d;document.querySelectorAll('[data-d]').forEach(function(b){b.classList.toggle('active',parseInt(b.getAttribute('data-d'))===d)})}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// SENSITIVITY
|
||
// ═══════════════════════════════════════════════════════
|
||
$('hs-d').onclick=function(){CFG.hS=Math.max(0.1,CFG.hS-0.1);$('hs-v').textContent=CFG.hS.toFixed(1)};
|
||
$('hs-u').onclick=function(){CFG.hS=Math.min(3,CFG.hS+0.1);$('hs-v').textContent=CFG.hS.toFixed(1)};
|
||
$('ms-d').onclick=function(){CFG.mS=Math.max(0.1,CFG.mS-0.1);$('ms-v').textContent=CFG.mS.toFixed(1)};
|
||
$('ms-u').onclick=function(){CFG.mS=Math.min(3,CFG.mS+0.1);$('ms-v').textContent=CFG.mS.toFixed(1)};
|
||
$('mr-d').onclick=function(){CFG.mRange=Math.max(0.25,CFG.mRange-0.25);$('mr-v').textContent=Math.round(CFG.mRange*100)+'%'};
|
||
$('mr-u').onclick=function(){CFG.mRange=Math.min(10,CFG.mRange+0.25);$('mr-v').textContent=Math.round(CFG.mRange*100)+'%'};
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// CAMERA FOCUS
|
||
// ═══════════════════════════════════════════════════════
|
||
function animCam(tgt,dist,dur){
|
||
dur=dur||600;var st=orbitTarget.clone(),sd=orbitDist,sth=orbitTheta,sph=orbitPhi,t0=performance.now();
|
||
function step(now){
|
||
var t=Math.min((now-t0)/dur,1),e=t<0.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;
|
||
orbitTarget.lerpVectors(st,tgt,e);orbitDist=sd+(dist-sd)*e;
|
||
orbitTheta=sth+(0-sth)*e;orbitPhi=sph+(0.1-sph)*e;
|
||
if(t<1) requestAnimationFrame(step);
|
||
}
|
||
requestAnimationFrame(step);
|
||
}
|
||
|
||
$('cam-head').onclick=function(){
|
||
if(!loadedModel) return;
|
||
if(A.head){
|
||
var wp=new THREE.Vector3();A.head.getWorldPosition(wp);
|
||
animCam(wp,1.0);
|
||
} else {
|
||
var b=new THREE.Box3().setFromObject(loadedModel),s=b.getSize(new THREE.Vector3());
|
||
var c=b.getCenter(new THREE.Vector3());c.y=b.max.y-s.y*0.15;
|
||
animCam(c,s.y*0.6);
|
||
}
|
||
};
|
||
$('cam-body').onclick=function(){
|
||
if(!loadedModel) return;
|
||
var b=new THREE.Box3().setFromObject(loadedModel),c=b.getCenter(new THREE.Vector3()),s=b.getSize(new THREE.Vector3());
|
||
animCam(c,Math.max(s.x,s.y,s.z)*2);
|
||
};
|
||
$('cam-reset').onclick=function(){
|
||
if(loadedModel){var b=new THREE.Box3().setFromObject(loadedModel),c=b.getCenter(new THREE.Vector3()),s=b.getSize(new THREE.Vector3());animCam(c,Math.max(s.x,s.y,s.z)*2)}
|
||
else animCam(new THREE.Vector3(0,0.3,0),3.5);
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// GAMEPAD
|
||
// ═══════════════════════════════════════════════════════
|
||
window.addEventListener('gamepadconnected',function(e){
|
||
$('gp-dot').className='sd on';$('gp-status').textContent=e.gamepad.id.substring(0,40);
|
||
$('gp-hint').textContent='Index '+e.gamepad.index+' · '+e.gamepad.buttons.length+' btn · '+e.gamepad.axes.length+' axes';
|
||
});
|
||
window.addEventListener('gamepaddisconnected',function(){$('gp-dot').className='sd off';$('gp-status').textContent='Disconnected';$('gp-hint').textContent='Press any button to reconnect'});
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// KEYBOARD
|
||
// ═══════════════════════════════════════════════════════
|
||
window.addEventListener('keydown',function(e){
|
||
if(e.target.tagName==='SELECT'||e.target.tagName==='INPUT')return;
|
||
S.keys[e.code]=true;if(e.code==='Space')e.preventDefault();
|
||
});
|
||
window.addEventListener('keyup',function(e){S.keys[e.code]=false});
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// INPUT → STATE → MODEL
|
||
// ═══════════════════════════════════════════════════════
|
||
function dz(v){return Math.abs(v)<CFG.dz?0:v}
|
||
|
||
function processInput(){
|
||
var k=S.keys,spd=2.5;
|
||
var kY=0,kP=0,kR=0,kM=0;
|
||
if(k['KeyD'])kY+=spd;if(k['KeyA'])kY-=spd;
|
||
if(k['KeyS'])kP+=spd;if(k['KeyW'])kP-=spd;
|
||
if(k['KeyQ'])kR+=spd;if(k['KeyE'])kR-=spd;
|
||
if(k['Space'])kM=1;
|
||
if(k['KeyR']){S.tY=S.tP=S.tR=S.tM=0;return}
|
||
|
||
var gY=0,gP=0,gR=0,gM=0;
|
||
var pads=navigator.getGamepads?navigator.getGamepads():[];
|
||
for(var pi=0;pi<pads.length;pi++){
|
||
var gp=pads[pi];if(!gp)continue;
|
||
if($('gp-dot').className.indexOf('off')>=0){
|
||
$('gp-dot').className='sd on';$('gp-status').textContent=gp.id.substring(0,40);
|
||
$('gp-hint').textContent=gp.buttons.length+' btn · '+gp.axes.length+' axes';
|
||
}
|
||
gY=-dz(gp.axes[0])*spd;gP=-dz(gp.axes[1])*spd;
|
||
if(gp.axes.length>2)gR=-dz(gp.axes[2])*spd;
|
||
if(gp.buttons.length>7&&gp.buttons[7])gM=Math.max(gM,gp.buttons[7].value);
|
||
if(gp.axes.length>5){var tv=(gp.axes[5]+1)/2;if(tv>0.05)gM=Math.max(gM,tv)}
|
||
if(gp.buttons.length>3&&gp.buttons[3].pressed){S.tY=S.tP=S.tR=S.tM=0;return}
|
||
break;
|
||
}
|
||
|
||
var yIn=Math.abs(kY)>Math.abs(gY)?kY:gY;
|
||
var pIn=Math.abs(kP)>Math.abs(gP)?kP:gP;
|
||
var rIn=Math.abs(kR)>Math.abs(gR)?kR:gR;
|
||
var mIn=Math.max(kM,gM);
|
||
|
||
S.tY+=yIn*CFG.hS*0.016*60;S.tP+=pIn*CFG.hS*0.016*60;S.tR+=rIn*CFG.hS*0.016*60;
|
||
S.tM=mIn*CFG.mS;
|
||
S.tY=THREE.MathUtils.clamp(S.tY,-CFG.yawMax,CFG.yawMax);
|
||
S.tP=THREE.MathUtils.clamp(S.tP,-CFG.pitchMax,CFG.pitchMax);
|
||
S.tR=THREE.MathUtils.clamp(S.tR,-CFG.rollMax,CFG.rollMax);
|
||
S.tM=THREE.MathUtils.clamp(S.tM,0,1);
|
||
}
|
||
|
||
function applySmoothing(){
|
||
S.cY+=(S.tY-S.cY)*CFG.sm;S.cP+=(S.tP-S.cP)*CFG.sm;
|
||
S.cR+=(S.tR-S.cR)*CFG.sm;S.cM+=(S.tM-S.cM)*CFG.msm;
|
||
}
|
||
|
||
function applyToModel(){
|
||
var yr=THREE.MathUtils.degToRad(S.cY),pr=THREE.MathUtils.degToRad(S.cP),rr=THREE.MathUtils.degToRad(S.cR);
|
||
|
||
if(A.head&&A.headI){
|
||
A.head.rotation.set(A.headI.x+pr, A.headI.y+yr, A.headI.z+rr);
|
||
}
|
||
if(A.neck&&A.neckI){
|
||
A.neck.rotation.set(A.neckI.x+pr*0.4, A.neckI.y+yr*0.4, A.neckI.z+rr*0.3);
|
||
}
|
||
if(A.jaw&&A.jawI){
|
||
var d=S.cM*CFG.mRange*CFG.jawAngle*CFG.jawDir;
|
||
A.jaw.rotation.set(
|
||
A.jawI.x+(CFG.jawAxis==='x'?d:0),
|
||
A.jawI.y+(CFG.jawAxis==='y'?d:0),
|
||
A.jawI.z+(CFG.jawAxis==='z'?d:0)
|
||
);
|
||
}
|
||
if(A.morphMesh&&A.morphIdx>=0){
|
||
A.morphMesh.morphTargetInfluences[A.morphIdx]=S.cM*CFG.mRange;
|
||
}
|
||
}
|
||
|
||
function updateHUD(){
|
||
$('v-yaw').textContent=S.cY.toFixed(1)+'°';
|
||
$('v-pitch').textContent=S.cP.toFixed(1)+'°';
|
||
$('v-roll').textContent=S.cR.toFixed(1)+'°';
|
||
var mp=(S.cM*CFG.mRange*100).toFixed(0);
|
||
$('v-mouth').textContent=mp+'%';$('mouth-bar').style.width=Math.min(100,S.cM*100)+'%';
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// MAIN LOOP
|
||
// ═══════════════════════════════════════════════════════
|
||
function animate(){
|
||
requestAnimationFrame(animate);
|
||
processInput();applySmoothing();applyToModel();updateHUD();updateCam();
|
||
// VRM update (required for spring bones, look-at, etc.)
|
||
if(vrmData&&vrmData.update) vrmData.update(0.016);
|
||
ren.render(scene,cam);
|
||
}
|
||
animate();
|
||
|
||
window.addEventListener('resize',function(){cam.aspect=innerWidth/innerHeight;cam.updateProjectionMatrix();ren.setSize(innerWidth,innerHeight)});
|
||
|
||
console.log('[HC] Initialized. Three.js r'+THREE.REVISION);
|
||
</script>
|
||
</body>
|
||
</html>
|