import * as THREE from 'three'; import * as TWEEN from '@tweenjs/tween.js'; const scene = new THREE.Scene(); scene.background = new THREE.Color( // 0xcccccc 'white' ); const clock = new THREE.Clock(); const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000); camera.position.set(0, 30, 50); camera.lookAt(0, 3, 0); const controls = new (THREE).OrbitControls(camera); const ambientLight = new THREE.AmbientLight(0xffffff, 1); scene.add(ambientLight); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const stats = new Stats(); document.body.appendChild(stats.dom); /// Anim mixer const mixers: THREE.AnimationMixer[] = []; class Assets { private static loadEggMtl(): Promise { return new Promise((resolve, reject) => { const loader: THREE.AnyLoader = new (THREE).MTLLoader(); loader.load( `models/Egg_from_Poly_uovo/Egg from Poly uovo.mtl`, (materials) => { materials.preload(); resolve(materials); }, (xhr) => {}, reject ); }); } private static loadEggObj(materials: THREE.Material[]): Promise { return new Promise((resolve, reject) => { const loader: THREE.AnyLoader = new (THREE).OBJLoader(); (loader).setMaterials(materials); loader.load( `models/Egg_from_Poly_uovo/Egg from Poly uovo.obj`, (object: THREE.Object3D) => { resolve(object); }, (xhr) => { // c.log(`${ xhr.loaded / xhr.total * 100 }% loaded`); }, (error) => { c.error(error); reject(error); } ); }); } static async loadEgg(): Promise { const materialCreator = await this.loadEggMtl(); return this.loadEggObj(materialCreator); } static loadEggGltf(): Promise { return new Promise((resolve, reject) => { const loader: THREE.AnyLoader = new (THREE).GLTFLoader(); loader.load( `models/Egg_gltf/Egg from Poly uovo copy.gltf`, (gltf) => { c.log(gltf); resolve(gltf.scene); } ); }); } static loadDogDae(): Promise<{ animations: THREE.AnimationClip[]; scene: THREE.Group; }> { /// In Dae/Collada: did not manage to get /// either the anims or the texture. return new Promise((resolve, reject) => { const loader: THREE.AnyLoader = new (THREE).ColladaLoader(); loader.load( `models/dog/pup_lohound.dae`, (collada) => { resolve(collada); } ); }); } static loadDogFbx(): Promise { return new Promise((resolve, reject) => { const loader: THREE.AnyLoader = new (THREE).FBXLoader(); loader.load( `models/dog_fbx/puppy-snapchat.fbx`, (fbx) => { resolve(fbx); } ); }); } static loadBoloss(): Promise<{ animations: THREE.AnimationClip[]; scene: THREE.Group; }> { return new Promise((resolve, reject) => { const loader: THREE.AnyLoader = new (THREE).ColladaLoader(); loader.load( `models/boloss/Boloss-3d v10.dae`, (collada) => { resolve(collada); } ); }); } } class TUtils { static boundingBox(o: THREE.Object3D): THREE.Box3 { const bbox = new THREE.Box3().setFromObject(o); return bbox; } static flushYZero(o: THREE.Object3D) { o.position.y = -(this.boundingBox(o)).min.y; } static perform(tween: TWEEN.Tween): Promise { return new Promise(resolve => { tween.onComplete(resolve).start(); }); } } (async () => { /** * scene construction */ const gridHelper = new THREE.GridHelper(100, 100); scene.add(gridHelper); const axesHelper = new THREE.AxesHelper(50); scene.add(axesHelper); { const egg = await Assets.loadEgg(); c.log(egg); egg.scale.setScalar(.2); egg.rotateX(-Math.PI / 2); egg.position.x = -18; TUtils.flushYZero(egg); const box = new THREE.BoxHelper(egg); scene.add(box); scene.add(egg); ///// Manually set the material, for fun. // const eggFace = egg.getObjectByName("CallKit-IconMask") as THREE.Mesh; // c.log(eggFace.material); // ((eggFace.material)).color.set(0x000000); } { const egg = await Assets.loadEggGltf(); c.log(egg); egg.scale.setScalar(100); egg.position.x = -28; TUtils.flushYZero(egg); egg.remove(egg.getObjectByName('Camera')!); scene.add(egg); // c.log(Utils.boundingBox(egg)); const box = new THREE.BoxHelper(egg, new THREE.Color('red')); scene.add(box); } { ////// dog_fbx const dog = await Assets.loadDogFbx(); // c.log((dog).animations); const mixer = new THREE.AnimationMixer(dog); const clip: THREE.AnimationClip = (dog).animations.find(clip => clip.name === "lohound|lohoundAction"); /// ^^ this is the main parent animation! Do not play all children. c.log(clip); mixer.clipAction(clip).play(); mixers.push(mixer); const container = new THREE.Group(); container.add(dog); container.scale.setScalar(0.007); /// <- scale a container, not the dog itself or it'll fuck the anims. container.position.x = -6; scene.add(container); const box = new THREE.BoxHelper(container, new THREE.Color('green')); scene.add(box); } { const boloss = (await Assets.loadBoloss()).scene; c.log(boloss); boloss.position.x = 16; TUtils.flushYZero(boloss); scene.add(boloss); const box = new THREE.BoxHelper(boloss, new THREE.Color('blue')); scene.add(box); /// Anims like in AudioBoloss const rootModel = boloss.getObjectByName(`SketchUp`)!; const pupilL = boloss.getObjectByName(`Pupil-left`)!; const pupilR = boloss.getObjectByName(`Pupil-right`)!; const pupils = new THREE.Group(); pupils.add(pupilL, pupilR); rootModel.add(pupils); (async () => { while (true) { const translatePupil = new TWEEN.Tween(pupils.position) .to({ x: "-1", y: "-1" }, 200) .easing(TWEEN.Easing.Quadratic.Out) ; const translatePupilRev = new TWEEN.Tween(pupils.position) .to({ x: "+1", y: "+1" }, 200) .easing(TWEEN.Easing.Quadratic.Out) ; await TUtils.perform(translatePupil); await Utils.wait(4, 1); await TUtils.perform(translatePupilRev); await Utils.wait(8, 3); } })(); const eyebrowL = boloss.getObjectByName(`Eyebrow-left`)!; const eyebrowR = boloss.getObjectByName(`Eyebrow-right`)!; const eyebrows = new THREE.Group(); eyebrows.add(eyebrowL, eyebrowR); rootModel.add(eyebrows); (async () => { while (true) { const scaleEyebrow = new TWEEN.Tween(eyebrows.scale) .to({ x: 1.08, y: 1.08, z: 1.08 }, 100) .easing(TWEEN.Easing.Quadratic.InOut) ; const scaleEyebrowRev = new TWEEN.Tween(eyebrows.scale) .to({ x: 1, y: 1, z: 1 }, 100) .easing(TWEEN.Easing.Quadratic.InOut) ; await Utils.wait(6, 6); await TUtils.perform(scaleEyebrow); await TUtils.perform(scaleEyebrowRev); await Utils.wait(0.14); await TUtils.perform(scaleEyebrow); await TUtils.perform(scaleEyebrowRev); } })(); (async () => { while (true) { const angle = Utils.randomFloat(-0.2, 0.3); const dummyL = new THREE.Object3D(); dummyL.rotateOnAxis(new THREE.Vector3(0, 1, 0.8), angle); const dummyR = new THREE.Object3D(); dummyR.rotateOnAxis(new THREE.Vector3(0, -1, -0.8), angle); /// ^^ exact same result as keeping the same vector and negating the angle. const rotateBrowL = new TWEEN.Tween(eyebrowL.rotation) .to({ x: dummyL.rotation.x, y: dummyL.rotation.y, z: dummyL.rotation.z, }, 300) ; const rotateBrowR = new TWEEN.Tween(eyebrowR.rotation) .to({ x: dummyR.rotation.x, y: dummyR.rotation.y, z: dummyR.rotation.z, }, 300) ; await Promise.all([ TUtils.perform(rotateBrowL), TUtils.perform(rotateBrowR), ]); await Utils.wait(1, 1); await Promise.all([ TUtils.perform( new TWEEN.Tween(eyebrowL.rotation).to({ x: 0, y: 0, z: 0 }, 300) ), TUtils.perform( new TWEEN.Tween(eyebrowR.rotation).to({ x: 0, y: 0, z: 0 }, 300) ), ]); await Utils.wait(1, 1); /// ^^ not the exact same behavior as in AudioBoloss (all waits are actually randoms there.) } })(); } })(); /** * MAIN() */ window.addEventListener('resize', onWindowResize, false); function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } function render() { const delta = clock.getDelta(); for (const mixer of mixers) { mixer.update(delta); } renderer.render(scene, camera); } function animate() { requestAnimationFrame(animate); TWEEN.update(); render(); stats.update(); } animate();