Spaces:
Sleeping
Sleeping
| import * as THREE from "three"; | |
| import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; | |
| import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js"; | |
| import { MeshoptDecoder } from "three/addons/libs/meshopt_decoder.module.js"; | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth * 0.66, window.innerHeight); | |
| document.getElementById("viewer").appendChild(renderer.domElement); | |
| scene.add(new THREE.AmbientLight(0xffffff, 0.6)); | |
| const light = new THREE.DirectionalLight(0xffffff, 0.8); | |
| light.position.set(0, 1, 2); | |
| scene.add(light); | |
| const ktx2 = new KTX2Loader() | |
| .setTranscoderPath("https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/basis/") | |
| .detectSupport(renderer); | |
| const loader = new GLTFLoader(); | |
| loader.setKTX2Loader(ktx2); | |
| loader.setMeshoptDecoder(MeshoptDecoder); | |
| let mesh; | |
| loader.load("facecap.glb", gltf => { | |
| scene.add(gltf.scene); | |
| gltf.scene.traverse(o => { | |
| if (o.morphTargetDictionary) mesh = o; | |
| }); | |
| const box = new THREE.Box3().setFromObject(gltf.scene); | |
| const size = box.getSize(new THREE.Vector3()).length(); | |
| const center = box.getCenter(new THREE.Vector3()); | |
| gltf.scene.position.sub(center); | |
| camera.position.z = size * 0.7; | |
| }); | |
| let analyser, data; | |
| async function speak(text) { | |
| const res = await fetch("/speak", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ text }) | |
| }); | |
| const { audio } = await res.json(); | |
| const audioEl = new Audio(audio); | |
| const ctx = new AudioContext(); | |
| const src = ctx.createMediaElementSource(audioEl); | |
| analyser = ctx.createAnalyser(); | |
| analyser.fftSize = 1024; | |
| data = new Uint8Array(analyser.frequencyBinCount); | |
| src.connect(analyser); | |
| analyser.connect(ctx.destination); | |
| audioEl.play(); | |
| audioEl.onended = () => { | |
| if (mesh?.morphTargetDictionary.mouthSmile_L !== undefined) { | |
| mesh.morphTargetInfluences[mesh.morphTargetDictionary.mouthSmile_L] = 0.7; | |
| mesh.morphTargetInfluences[mesh.morphTargetDictionary.mouthSmile_R] = 0.7; | |
| } | |
| }; | |
| } | |
| document.getElementById("speak").onclick = () => { | |
| speak(document.getElementById("text").value); | |
| }; | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (mesh && analyser) { | |
| analyser.getByteFrequencyData(data); | |
| const energy = data.reduce((a,b)=>a+b,0)/data.length; | |
| const jaw = mesh.morphTargetDictionary.jawOpen; | |
| if (jaw !== undefined) { | |
| mesh.morphTargetInfluences[jaw] = Math.min(energy / 180, 1); | |
| } | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |