FBX Animations
Video Lecture
Description
In this exercise, we import a different FBX model, and we also import several animation clips for the model. We then create buttons to smoothly transition the model between each animation clip.
Note
For this exercise, you should download the model "Vanguard By T. Choonyung" from Mixamo and save it into your ./dist/client/models folder. Save it using the options FBX(.fbx) and T-Pose.
Also download the animations, rename them and save them into your ./dist/client/models folder. Save them using the options FBX(.fbx) and Without Skin.
- Samba Dancing (In Place Version) --> Save as vanguard@samba.fbx
- Belly Dancing --> Save as vanguard@bellydance.fbx
- Goofy Running --> Save as vanguard@goofyrunning.fbx
Start Code
./src/server/server.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import express from "express" import path from "path" import http from "http" const port: number = 3000 class App { private server: http.Server private port: number constructor(port: number) { this.port = port const app = express() app.use(express.static(path.join(__dirname, '../client'))) app.use('/build/three.module.js', express.static(path.join(__dirname, '../../node_modules/three/build/three.module.js'))) app.use('/jsm/controls/OrbitControls', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/controls/OrbitControls.js'))) app.use('/jsm/loaders/FBXLoader', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/loaders/FBXLoader.js'))) app.use('/jsm/libs/inflate.module.min.js', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/libs/inflate.module.min.js'))) app.use('/jsm/curves/NURBSCurve.js', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/curves/NURBSCurve.js'))) app.use('/jsm/curves/NURBSUtils.js', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/curves/NURBSUtils.js'))) app.use('/jsm/libs/stats.module', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/libs/stats.module.js'))) app.use('/jsm/libs/dat.gui.module', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/libs/dat.gui.module.js'))) this.server = new http.Server(app); } public Start() { this.server.listen(this.port, () => { console.log(`Server listening on port ${this.port}.`) }) } } new App(port).Start() |
./src/client/client.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | import * as THREE from '/build/three.module.js' import { OrbitControls } from '/jsm/controls/OrbitControls' import { FBXLoader } from '/jsm/loaders/FBXLoader' import Stats from '/jsm/libs/stats.module' import { GUI } from '/jsm/libs/dat.gui.module' const scene: THREE.Scene = new THREE.Scene() const axesHelper = new THREE.AxesHelper(5) scene.add(axesHelper) var light = new THREE.PointLight(); light.position.set(2.5, 7.5, 15) scene.add(light); const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) camera.position.set(0.8, 1.4, 1.0) const renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer() renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(renderer.domElement) const controls = new OrbitControls(camera, renderer.domElement) controls.screenSpacePanning = true controls.target.set(0, 1, 0) let mixer: THREE.AnimationMixer let modelReady = false; let animationActions: THREE.AnimationAction[] = new Array() let activeAction: THREE.AnimationAction let lastAction: THREE.AnimationAction const fbxLoader: FBXLoader = new FBXLoader(); fbxLoader.load( 'models/vanguard_t_choonyung.fbx', (object) => { object.scale.set(.01, .01, .01) mixer = new THREE.AnimationMixer(object); let animationAction = mixer.clipAction((object as any).animations[0]); animationActions.push(animationAction) animationsFolder.add(animations, "default") activeAction = animationActions[0] scene.add(object); //add an animation from another file // fbxLoader.load('models/vanguard@samba.fbx', // (object) => { // console.log("loaded samba") // let animationAction = mixer.clipAction((object as any).animations[0]); // animationActions.push(animationAction) // animationsFolder.add(animations, "samba") // //add an animation from another file // fbxLoader.load('models/vanguard@bellydance.fbx', // (object) => { // console.log("loaded bellydance") // let animationAction = mixer.clipAction((object as any).animations[0]); // animationActions.push(animationAction) // animationsFolder.add(animations, "bellydance") // //add an animation from another file // fbxLoader.load('models/vanguard@goofyrunning.fbx', // (object) => { // console.log("loaded goofyrunning"); // (object as any).animations[0].tracks.shift() //delete the specific track that moves the object forward while running // //console.dir((object as any).animations[0]) // let animationAction = mixer.clipAction((object as any).animations[0]); // animationActions.push(animationAction) // animationsFolder.add(animations, "goofyrunning") // modelReady = true // }, // (xhr) => { // console.log((xhr.loaded / xhr.total * 100) + '% loaded') // }, // (error) => { // console.log(error); // } // ) // }, // (xhr) => { // console.log((xhr.loaded / xhr.total * 100) + '% loaded') // }, // (error) => { // console.log(error); // } // ) // }, // (xhr) => { // console.log((xhr.loaded / xhr.total * 100) + '% loaded') // }, // (error) => { // console.log(error); // } // ) }, (xhr) => { console.log((xhr.loaded / xhr.total * 100) + '% loaded') }, (error) => { console.log(error); } ) window.addEventListener('resize', onWindowResize, false) function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) render() } const stats = Stats() document.body.appendChild(stats.dom) var animations = { default: function () { setAction(animationActions[0]) }, samba: function () { setAction(animationActions[1]) }, bellydance: function () { setAction(animationActions[2]) }, goofyrunning: function () { setAction(animationActions[3]) }, } const setAction = (toAction: THREE.AnimationAction) => { if (toAction != activeAction) { lastAction = activeAction activeAction = toAction lastAction.stop() //lastAction.fadeOut(1) activeAction.reset() //activeAction.fadeIn(1) activeAction.play() } } const gui = new GUI() const animationsFolder = gui.addFolder("Animations") animationsFolder.open() const clock: THREE.Clock = new THREE.Clock() var animate = function () { requestAnimationFrame(animate) controls.update() if (modelReady) mixer.update(clock.getDelta()); render() stats.update() }; function render() { renderer.render(scene, camera) } animate(); |
Final Code
./src/client/client.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | import * as THREE from '/build/three.module.js' import { OrbitControls } from '/jsm/controls/OrbitControls' import { FBXLoader } from '/jsm/loaders/FBXLoader' import Stats from '/jsm/libs/stats.module' import { GUI } from '/jsm/libs/dat.gui.module' const scene: THREE.Scene = new THREE.Scene() const axesHelper = new THREE.AxesHelper(5) scene.add(axesHelper) var light = new THREE.PointLight(); light.position.set(2.5, 7.5, 15) scene.add(light); const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) camera.position.set(0.8, 1.4, 1.0) const renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer() renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(renderer.domElement) const controls = new OrbitControls(camera, renderer.domElement) controls.screenSpacePanning = true controls.target.set(0, 1, 0) let mixer: THREE.AnimationMixer let modelReady = false; let animationActions: THREE.AnimationAction[] = new Array() let activeAction: THREE.AnimationAction let lastAction: THREE.AnimationAction const fbxLoader: FBXLoader = new FBXLoader(); fbxLoader.load( 'models/vanguard_t_choonyung.fbx', (object) => { object.scale.set(.01, .01, .01) mixer = new THREE.AnimationMixer(object); let animationAction = mixer.clipAction((object as any).animations[0]); animationActions.push(animationAction) animationsFolder.add(animations, "default") activeAction = animationActions[0] scene.add(object); //add an animation from another file fbxLoader.load('models/vanguard@samba.fbx', (object) => { console.log("loaded samba") let animationAction = mixer.clipAction((object as any).animations[0]); animationActions.push(animationAction) animationsFolder.add(animations, "samba") //add an animation from another file fbxLoader.load('models/vanguard@bellydance.fbx', (object) => { console.log("loaded bellydance") let animationAction = mixer.clipAction((object as any).animations[0]); animationActions.push(animationAction) animationsFolder.add(animations, "bellydance") //add an animation from another file fbxLoader.load('models/vanguard@goofyrunning.fbx', (object) => { console.log("loaded goofyrunning"); (object as any).animations[0].tracks.shift() //console.dir((object as any).animations[0]) let animationAction = mixer.clipAction((object as any).animations[0]); animationActions.push(animationAction) animationsFolder.add(animations, "goofyrunning") //console.dir(animationAction) modelReady = true }, (xhr) => { console.log((xhr.loaded / xhr.total * 100) + '% loaded') }, (error) => { console.log(error); } ) }, (xhr) => { console.log((xhr.loaded / xhr.total * 100) + '% loaded') }, (error) => { console.log(error); } ) }, (xhr) => { console.log((xhr.loaded / xhr.total * 100) + '% loaded') }, (error) => { console.log(error); } ) }, (xhr) => { console.log((xhr.loaded / xhr.total * 100) + '% loaded') }, (error) => { console.log(error); } ) window.addEventListener('resize', onWindowResize, false) function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) render() } const stats = Stats() document.body.appendChild(stats.dom) var animations = { default: function () { setAction(animationActions[0]) }, samba: function () { setAction(animationActions[1]) }, bellydance: function () { setAction(animationActions[2]) }, goofyrunning: function () { setAction(animationActions[3]) }, } const setAction = (toAction: THREE.AnimationAction) => { if (toAction != activeAction) { lastAction = activeAction activeAction = toAction //lastAction.stop() lastAction.fadeOut(1) activeAction.reset() activeAction.fadeIn(1) activeAction.play() } } const gui = new GUI() const animationsFolder = gui.addFolder("Animations") animationsFolder.open() const clock: THREE.Clock = new THREE.Clock() var animate = function () { requestAnimationFrame(animate) controls.update() if (modelReady) mixer.update(clock.getDelta()); render() stats.update() }; function render() { renderer.render(scene, camera) } animate(); |