script.js
1 // setup 2 3 //expression setup 4 var expressionyay = 0; 5 var expressionoof = 0; 6 var expressionlimityay = 0.5; 7 var expressionlimitoof = 0.5; 8 var expressionease = 100; 9 var expressionintensity = 0.75; 10 11 //interface values 12 if (localStorage.localvalues) { 13 var initvalues = true; 14 var mouththreshold = Number(localStorage.mouththreshold) ; 15 var mouthboost = Number(localStorage.mouthboost) ; 16 var bodythreshold = Number(localStorage.bodythreshold) ; 17 var bodymotion = Number(localStorage.bodymotion) ; 18 var expression = Number(localStorage.expression) ; 19 } else { 20 var mouththreshold = 10; 21 var mouthboost = 10; 22 var bodythreshold = 10; 23 var bodymotion = 10; 24 var expression = 80; 25 } 26 27 // setup three-vrm 28 29 // renderer 30 const renderer = new THREE.WebGLRenderer({ alpha: true , antialias: true ,powerPreference: "low-power" }); 31 renderer.setSize(window.innerWidth, window.innerHeight); 32 renderer.setPixelRatio(window.devicePixelRatio); 33 document.body.appendChild(renderer.domElement); 34 35 // camera 36 const camera = new THREE.PerspectiveCamera( 30.0, window.innerWidth / window.innerHeight, 0.1, 20.0 ); 37 camera.position.set(0.0, 1.45, 0.75); 38 39 // camera controls 40 const controls = new THREE.OrbitControls(camera, renderer.domElement); 41 controls.screenSpacePanning = true; 42 controls.target.set(0.0, 1.45, 0.0); 43 controls.update(); 44 45 // scene 46 const scene = new THREE.Scene(); 47 48 // light 49 const light = new THREE.DirectionalLight(0xffffff); 50 light.position.set(1.0, 1.0, 1.0).normalize(); 51 scene.add(light); 52 53 // lookat target 54 const lookAtTarget = new THREE.Object3D(); 55 camera.add(lookAtTarget); 56 57 // gltf and vrm 58 let currentVrm = undefined; 59 const loader = new THREE.GLTFLoader(); 60 61 function load( url ) { 62 63 loader.crossOrigin = 'anonymous'; 64 loader.load( 65 66 url, 67 68 ( gltf ) => { 69 70 //THREE.VRMUtils.removeUnnecessaryVertices( gltf.scene ); Vroid VRM can't handle this for some reason 71 THREE.VRMUtils.removeUnnecessaryJoints( gltf.scene ); 72 73 THREE.VRM.from( gltf ).then( ( vrm ) => { 74 75 if ( currentVrm ) { 76 77 scene.remove( currentVrm.scene ); 78 currentVrm.dispose(); 79 80 } 81 82 currentVrm = vrm; 83 scene.add( vrm.scene ); 84 85 vrm.humanoid.getBoneNode( THREE.VRMSchema.HumanoidBoneName.Hips ).rotation.y = Math.PI; 86 vrm.springBoneManager.reset(); 87 88 // un-T-pose 89 90 vrm.humanoid.getBoneNode( 91 THREE.VRMSchema.HumanoidBoneName.RightUpperArm 92 ).rotation.z = 250; 93 94 vrm.humanoid.getBoneNode( 95 THREE.VRMSchema.HumanoidBoneName.RightLowerArm 96 ).rotation.z = -0.2; 97 98 vrm.humanoid.getBoneNode( 99 THREE.VRMSchema.HumanoidBoneName.LeftUpperArm 100 ).rotation.z = -250; 101 102 vrm.humanoid.getBoneNode( 103 THREE.VRMSchema.HumanoidBoneName.LeftLowerArm 104 ).rotation.z = 0.2; 105 106 // randomise init positions 107 108 function randomsomesuch() { 109 return (Math.random() - 0.5) / 10; 110 } 111 112 vrm.humanoid.getBoneNode( 113 THREE.VRMSchema.HumanoidBoneName.Head 114 ).rotation.x = randomsomesuch(); 115 vrm.humanoid.getBoneNode( 116 THREE.VRMSchema.HumanoidBoneName.Head 117 ).rotation.y = randomsomesuch(); 118 vrm.humanoid.getBoneNode( 119 THREE.VRMSchema.HumanoidBoneName.Head 120 ).rotation.z = randomsomesuch(); 121 122 vrm.humanoid.getBoneNode( 123 THREE.VRMSchema.HumanoidBoneName.Neck 124 ).rotation.x = randomsomesuch(); 125 vrm.humanoid.getBoneNode( 126 THREE.VRMSchema.HumanoidBoneName.Neck 127 ).rotation.y = randomsomesuch(); 128 vrm.humanoid.getBoneNode( 129 THREE.VRMSchema.HumanoidBoneName.Neck 130 ).rotation.z = randomsomesuch(); 131 132 vrm.humanoid.getBoneNode( 133 THREE.VRMSchema.HumanoidBoneName.Spine 134 ).rotation.x = randomsomesuch(); 135 vrm.humanoid.getBoneNode( 136 THREE.VRMSchema.HumanoidBoneName.Spine 137 ).rotation.y = randomsomesuch(); 138 vrm.humanoid.getBoneNode( 139 THREE.VRMSchema.HumanoidBoneName.Spine 140 ).rotation.z = randomsomesuch(); 141 142 vrm.lookAt.target = lookAtTarget; 143 vrm.springBoneManager.reset(); 144 145 console.log(vrm); 146 } ); 147 148 }, 149 150 ( progress ) => console.log( 'Loading model...', 100.0 * ( progress.loaded / progress.total ), '%' ), 151 152 ( error ) => console.error( error ) 153 154 ); 155 156 } 157 158 // beware of CORS errors when using this locally. If you can't https, import the required libraries. 159 load( './assets/VU-VRM-elf.vrm' ); 160 161 // grid / axis helpers 162 // const gridHelper = new THREE.GridHelper( 10, 10 ); 163 // scene.add( gridHelper ); 164 // const axesHelper = new THREE.AxesHelper( 5 ); 165 // scene.add( axesHelper ); 166 167 // animate 168 169 const clock = new THREE.Clock(); 170 171 function animate() { 172 requestAnimationFrame(animate); 173 174 const deltaTime = clock.getDelta(); 175 176 if (currentVrm) { 177 // update vrm 178 currentVrm.update(deltaTime); 179 } 180 181 renderer.render(scene, camera); 182 } 183 184 animate(); 185 186 187 188 189 190 //Analyser 191 analyser = audioContext.createAnalyser(); 192 //microphone = audioContext.createMediaElementSource(audio); 193 javascriptNode = audioContext.createScriptProcessor(256, 1, 1); 194 195 analyser.smoothingTimeConstant = 0.5; 196 analyser.fftSize = 1024; 197 198 //microphone.connect(analyser); 199 analyser.connect(javascriptNode); 200 javascriptNode.connect(audioContext.destination); 201 202 javascriptNode.onaudioprocess = function () { 203 var array = new Uint8Array(analyser.frequencyBinCount); 204 analyser.getByteFrequencyData(array); 205 var values = 0; 206 207 var length = array.length; 208 for (var i = 0; i < length; i++) { 209 values += array[i]; 210 } 211 212 // audio in expressed as one number 213 var average = values / length; 214 var inputvolume = average; 215 216 // audio in spectrum expressed as array 217 // console.log(array.toString()); 218 // useful for mouth shape variance 219 220 // move the interface slider 221 document.getElementById("inputlevel").value = inputvolume; 222 223 224 225 // mic based / endless animations (do stuff) 226 227 if (currentVrm != undefined){ //best to be sure 228 229 // talk 230 231 if (talktime == true){ 232 // todo: more vowelshapes 233 var voweldamp = 53; 234 var vowelmin = 12; 235 if (inputvolume > (mouththreshold * 2)) { 236 currentVrm.blendShapeProxy.setValue( 237 THREE.VRMSchema.BlendShapePresetName.A, 238 ( 239 (average - vowelmin) / voweldamp) * (mouthboost/10) 240 ); 241 242 }else{ 243 currentVrm.blendShapeProxy.setValue( 244 THREE.VRMSchema.BlendShapePresetName.A, 0 245 ); 246 }} 247 248 249 // move body 250 251 // todo: replace with ease-to-target behaviour 252 var damping = 750/(bodymotion/10); 253 var springback = 1.001; 254 255 if (average > (1 * bodythreshold)) { 256 currentVrm.humanoid.getBoneNode( 257 THREE.VRMSchema.HumanoidBoneName.Head 258 ).rotation.x += (Math.random() - 0.5) / damping; 259 currentVrm.humanoid.getBoneNode( 260 THREE.VRMSchema.HumanoidBoneName.Head 261 ).rotation.x /= springback; 262 currentVrm.humanoid.getBoneNode( 263 THREE.VRMSchema.HumanoidBoneName.Head 264 ).rotation.y += (Math.random() - 0.5) / damping; 265 currentVrm.humanoid.getBoneNode( 266 THREE.VRMSchema.HumanoidBoneName.Head 267 ).rotation.y /= springback; 268 currentVrm.humanoid.getBoneNode( 269 THREE.VRMSchema.HumanoidBoneName.Head 270 ).rotation.z += (Math.random() - 0.5) / damping; 271 currentVrm.humanoid.getBoneNode( 272 THREE.VRMSchema.HumanoidBoneName.Head 273 ).rotation.z /= springback; 274 275 currentVrm.humanoid.getBoneNode( 276 THREE.VRMSchema.HumanoidBoneName.Neck 277 ).rotation.x += (Math.random() - 0.5) / damping; 278 currentVrm.humanoid.getBoneNode( 279 THREE.VRMSchema.HumanoidBoneName.Neck 280 ).rotation.x /= springback; 281 currentVrm.humanoid.getBoneNode( 282 THREE.VRMSchema.HumanoidBoneName.Neck 283 ).rotation.y += (Math.random() - 0.5) / damping; 284 currentVrm.humanoid.getBoneNode( 285 THREE.VRMSchema.HumanoidBoneName.Neck 286 ).rotation.y /= springback; 287 currentVrm.humanoid.getBoneNode( 288 THREE.VRMSchema.HumanoidBoneName.Neck 289 ).rotation.z += (Math.random() - 0.5) / damping; 290 currentVrm.humanoid.getBoneNode( 291 THREE.VRMSchema.HumanoidBoneName.Neck 292 ).rotation.z /= springback; 293 294 currentVrm.humanoid.getBoneNode( 295 THREE.VRMSchema.HumanoidBoneName.UpperChest 296 ).rotation.x += (Math.random() - 0.5) / damping; 297 currentVrm.humanoid.getBoneNode( 298 THREE.VRMSchema.HumanoidBoneName.UpperChest 299 ).rotation.x /= springback; 300 currentVrm.humanoid.getBoneNode( 301 THREE.VRMSchema.HumanoidBoneName.UpperChest 302 ).rotation.y += (Math.random() - 0.5) / damping; 303 currentVrm.humanoid.getBoneNode( 304 THREE.VRMSchema.HumanoidBoneName.UpperChest 305 ).rotation.y /= springback; 306 currentVrm.humanoid.getBoneNode( 307 THREE.VRMSchema.HumanoidBoneName.UpperChest 308 ).rotation.z += (Math.random() - 0.5) / damping; 309 currentVrm.humanoid.getBoneNode( 310 THREE.VRMSchema.HumanoidBoneName.UpperChest 311 ).rotation.z /= springback; 312 313 currentVrm.humanoid.getBoneNode( 314 THREE.VRMSchema.HumanoidBoneName.RightShoulder 315 ).rotation.x += (Math.random() - 0.5) / damping; 316 currentVrm.humanoid.getBoneNode( 317 THREE.VRMSchema.HumanoidBoneName.RightShoulder 318 ).rotation.x /= springback; 319 currentVrm.humanoid.getBoneNode( 320 THREE.VRMSchema.HumanoidBoneName.RightShoulder 321 ).rotation.y += (Math.random() - 0.5) / damping; 322 currentVrm.humanoid.getBoneNode( 323 THREE.VRMSchema.HumanoidBoneName.RightShoulder 324 ).rotation.y /= springback; 325 currentVrm.humanoid.getBoneNode( 326 THREE.VRMSchema.HumanoidBoneName.RightShoulder 327 ).rotation.z += (Math.random() - 0.5) / damping; 328 currentVrm.humanoid.getBoneNode( 329 THREE.VRMSchema.HumanoidBoneName.RightShoulder 330 ).rotation.z /= springback; 331 332 currentVrm.humanoid.getBoneNode( 333 THREE.VRMSchema.HumanoidBoneName.LeftShoulder 334 ).rotation.x += (Math.random() - 0.5) / damping; 335 currentVrm.humanoid.getBoneNode( 336 THREE.VRMSchema.HumanoidBoneName.LeftShoulder 337 ).rotation.x /= springback; 338 currentVrm.humanoid.getBoneNode( 339 THREE.VRMSchema.HumanoidBoneName.LeftShoulder 340 ).rotation.y += (Math.random() - 0.5) / damping; 341 currentVrm.humanoid.getBoneNode( 342 THREE.VRMSchema.HumanoidBoneName.LeftShoulder 343 ).rotation.y /= springback; 344 currentVrm.humanoid.getBoneNode( 345 THREE.VRMSchema.HumanoidBoneName.LeftShoulder 346 ).rotation.z += (Math.random() - 0.5) / damping; 347 currentVrm.humanoid.getBoneNode( 348 THREE.VRMSchema.HumanoidBoneName.LeftShoulder 349 ).rotation.z /= springback; 350 351 } 352 353 // yay/oof expression drift 354 expressionyay += (Math.random() - 0.5) / expressionease; 355 if(expressionyay > expressionlimityay){expressionyay=expressionlimityay}; 356 if(expressionyay < 0){expressionyay=0}; 357 currentVrm.blendShapeProxy.setValue(THREE.VRMSchema.BlendShapePresetName.Fun, expressionyay); 358 expressionoof += (Math.random() - 0.5) / expressionease; 359 if(expressionoof > expressionlimitoof){expressionoof=expressionlimitoof}; 360 if(expressionoof < 0){expressionoof=0}; 361 currentVrm.blendShapeProxy.setValue(THREE.VRMSchema.BlendShapePresetName.Angry, expressionoof); 362 363 } 364 365 366 367 368 369 //look at camera is more efficient on blink 370 lookAtTarget.position.x = camera.position.x; 371 lookAtTarget.position.y = ((camera.position.y-camera.position.y-camera.position.y)/2)+0.5; 372 373 }; // end fn stream 374 375 376 377 378 379 // blink 380 381 function blink() { 382 var blinktimeout = Math.floor(Math.random() * 250) + 50; 383 lookAtTarget.position.y = 384 camera.position.y - camera.position.y * 2 + 1.25; 385 386 setTimeout(() => { 387 currentVrm.blendShapeProxy.setValue( 388 THREE.VRMSchema.BlendShapePresetName.BlinkL, 389 0 390 ); 391 currentVrm.blendShapeProxy.setValue( 392 THREE.VRMSchema.BlendShapePresetName.BlinkR, 393 0 394 ); 395 }, blinktimeout); 396 397 currentVrm.blendShapeProxy.setValue( 398 THREE.VRMSchema.BlendShapePresetName.BlinkL, 399 1 400 ); 401 currentVrm.blendShapeProxy.setValue( 402 THREE.VRMSchema.BlendShapePresetName.BlinkR, 403 1 404 ); 405 } 406 407 408 409 // loop blink timing 410 (function loop() { 411 var rand = Math.round(Math.random() * 10000) + 1000; 412 setTimeout(function () { 413 blink(); 414 loop(); 415 }, rand); 416 })(); 417 418 // drag and drop + file handler 419 window.addEventListener( 'dragover', function( event ) { 420 event.preventDefault(); 421 } ); 422 423 window.addEventListener( 'drop', function( event ) { 424 event.preventDefault(); 425 426 // read given file then convert it to blob url 427 const files = event.dataTransfer.files; 428 if ( !files ) { return; } 429 const file = files[0]; 430 if ( !file ) { return; } 431 const blob = new Blob( [ file ], { type: "application/octet-stream" } ); 432 const url = URL.createObjectURL( blob ); 433 load( url ); 434 } ); 435 436 437 // handle window resizes 438 439 440 window.addEventListener( 'resize', onWindowResize, false ); 441 442 function onWindowResize(){ 443 444 camera.aspect = window.innerWidth / window.innerHeight; 445 camera.updateProjectionMatrix(); 446 447 renderer.setSize( window.innerWidth, window.innerHeight ); 448 449 } 450 // interface handling 451 452 var talktime = true; 453 454 function interface() { 455 456 if (initvalues == true){ 457 if (localStorage.localvalues) { 458 initvalues = false; 459 document.getElementById("mouththreshold").value = mouththreshold; 460 document.getElementById("mouthboost").value = mouthboost; 461 document.getElementById("bodythreshold").value = bodythreshold; 462 document.getElementById("bodymotion").value = bodymotion; 463 document.getElementById("expression").value = expression; 464 }} 465 466 mouththreshold = document.getElementById("mouththreshold").value; 467 mouthboost = document.getElementById("mouthboost").value; 468 bodythreshold = document.getElementById("bodythreshold").value; 469 bodymotion = document.getElementById("bodymotion").value; 470 471 expression = document.getElementById("expression").value; 472 expressionlimityay = (expression); 473 expressionlimitoof = (100 - expression); 474 expressionlimityay = expressionlimityay/100; 475 expressionlimitoof = expressionlimitoof/100; 476 expressionlimityay = expressionlimityay*expressionintensity; 477 expressionlimitoof = expressionlimitoof*expressionintensity; 478 479 console.log("Expression " + expressionyay + " yay / " + expressionoof + " oof"); 480 console.log("Expression mix " + expressionlimityay + " yay / " + expressionlimitoof + " oof"); 481 482 // store it too 483 localStorage.localvalues = 1; 484 localStorage.mouththreshold = mouththreshold; 485 localStorage.mouthboost = mouthboost; 486 localStorage.bodythreshold = bodythreshold; 487 localStorage.bodymotion = bodymotion; 488 localStorage.expression = expression; 489 490 } 491 492 // click to dismiss non-vrm divs 493 function hideinterface() { 494 495 var a = document.getElementById("backplate"); 496 var b = document.getElementById("interface"); 497 var x = document.getElementById("infobar"); 498 var y = document.getElementById("credits"); 499 a.style.display = "none"; 500 b.style.display = "none"; 501 x.style.display = "none"; 502 y.style.display = "none"; 503 504 } 505 506 // click to dismiss non-interface divs 507 function hideinfo() { 508 509 var a = document.getElementById("backplate"); 510 var x = document.getElementById("infobar"); 511 var y = document.getElementById("credits"); 512 a.style.display = "none"; 513 x.style.display = "none"; 514 y.style.display = "none"; 515 516 } 517 518 // load file from user picker 519 function dofile(){ 520 var file = document.querySelector('input[type=file]').files[0]; 521 if ( !file ) { return; } 522 const blob = new Blob( [ file ], { type: "application/octet-stream" } ); 523 const url = URL.createObjectURL( blob ); 524 load( url ); 525 } 526 // end 527 528 // wait to trigger interface and load init values 529 530 setTimeout(() => { interface(); }, 500); 531 532 //ok