| <!doctype html> |
| <html lang="es"> |
| <head> |
| <meta charset="utf-8" /> |
| <title>Avatar 3D + TTS + Lipsync (ESM Local)</title> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <style> |
| html,body{margin:0;height:100%;background:#0b0d12;color:#e8e8e8;font-family:system-ui,Arial} |
| #canvas-wrap{position:fixed;inset:0} |
| #status{ |
| position:fixed;top:10px;left:10px;z-index:99; |
| background:rgba(0,0,0,.6);border:1px solid #2a2f3a;border-radius:8px; |
| padding:10px 12px;font-size:12px;max-width:60vw;white-space:pre-wrap |
| } |
| </style> |
| </head> |
| <body> |
| <div id="canvas-wrap"></div> |
| <div id="status">⏳ Cargando módulos locales…</div> |
|
|
| <script type="module"> |
| import * as THREE from "./libs/three.module.js"; |
| import { OrbitControls } from "./libs/OrbitControls.js"; |
| import { GLTFLoader } from "./libs/GLTFLoader.js"; |
| |
| const S = document.getElementById('status'); |
| const log = (...a)=>{ S.textContent += "\n" + a.join(' ') }; |
| const set = (t)=>{ S.textContent = t }; |
| |
| set("✅ Módulos importados (desde ./libs)"); |
| |
| const wrap = document.getElementById('canvas-wrap'); |
| const scene = new THREE.Scene(); |
| scene.background = new THREE.Color(0x0b0d12); |
| |
| const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 200); |
| camera.position.set(0,1.5,3); |
| |
| const renderer = new THREE.WebGLRenderer({ antialias:true }); |
| renderer.setSize(innerWidth, innerHeight); |
| wrap.appendChild(renderer.domElement); |
| |
| const controls = new OrbitControls(camera, renderer.domElement); |
| controls.target.set(0,1,0); controls.update(); |
| |
| scene.add(new THREE.HemisphereLight(0xffffff,0x222233,1)); |
| const dir = new THREE.DirectionalLight(0xffffff,1); |
| dir.position.set(3,5,3); scene.add(dir); |
| |
| const cube = new THREE.Mesh( |
| new THREE.BoxGeometry(0.8,0.8,0.8), |
| new THREE.MeshStandardMaterial({ color:0x44aa88 }) |
| ); |
| cube.position.set(-1,1,0); scene.add(cube); |
| |
| addEventListener("resize", ()=>{ |
| camera.aspect = innerWidth/innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(innerWidth, innerHeight); |
| }); |
| |
| function animate(){ |
| requestAnimationFrame(animate); |
| cube.rotation.y += 0.01; cube.rotation.x += 0.005; |
| renderer.render(scene, camera); |
| } |
| animate(); |
| |
| function fitToView(object3D){ |
| const box = new THREE.Box3().setFromObject(object3D); |
| if (box.isEmpty()) return; |
| const size = new THREE.Vector3(); box.getSize(size); |
| const center = new THREE.Vector3(); box.getCenter(center); |
| object3D.position.sub(center); |
| const maxDim = Math.max(size.x,size.y,size.z); |
| const dist = (maxDim/2) / Math.tan(THREE.MathUtils.degToRad(camera.fov/2)) * 1.4; |
| camera.position.set(0, Math.max(0.8, maxDim*0.25), dist); |
| controls.target.set(0,0.6,0); controls.update(); |
| } |
| |
| const loader = new GLTFLoader(); |
| set(S.textContent + "\n⏳ Cargando persona.glb…"); |
| |
| loader.load("persona.glb",(gltf)=>{ |
| const obj=gltf.scene; obj.scale.set(1,1,1); |
| scene.add(obj); fitToView(obj); |
| log("✅ persona.glb cargado"); |
| },undefined,(err)=>{ |
| log("⚠️ persona.glb falló → usando casco fallback"); |
| loader.load("https://rawcdn.githack.com/KhronosGroup/glTF-Sample-Models/master/2.0/DamagedHelmet/glTF-Binary/DamagedHelmet.glb", |
| (gltf)=>{ |
| scene.add(gltf.scene); fitToView(gltf.scene); |
| log("✅ Fallback cargado"); |
| } |
| ); |
| }); |
| </script> |
| </body> |
| </html> |
|
|