GLTF Custom Animations
We don't need to download animations from other websites, we can create our own.
Using Blender, you can create a model and then adjust the positions, scales and rotations of its parts by creating key frames on the timeline editor.
Test the animations works by using the play options on the timeline editor in Blender, and then export your model as GLB(preferred) or GLTF with animation options selected for the export.
After exporting your model, you can drag the GLB/GLTF file from your filesystem, onto this example scene below. It will read the file and create a new checkbox for every animation clip that it finds. You can enable/disable each animation.
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 | 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/GLTFLoader', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/loaders/GLTFLoader.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 | import * as THREE from '/build/three.module.js' import { OrbitControls } from '/jsm/controls/OrbitControls' import { GLTF, GLTFLoader } from '/jsm/loaders/GLTFLoader' import Stats from '/jsm/libs/stats.module' const scene: THREE.Scene = new THREE.Scene() const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 1000) camera.position.set(4, 4, 4) 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) let mixer: THREE.AnimationMixer let modelReady = false; const gltfLoader: GLTFLoader = new GLTFLoader(); const dropzone = document.getElementById("dropzone") as HTMLDivElement dropzone.ondragover = dropzone.ondragenter = function (evt) { evt.preventDefault() }; dropzone.ondrop = function (evt: DragEvent) { evt.stopPropagation() evt.preventDefault() //clear the scene for (let i = scene.children.length - 1; i >= 0; i--) { scene.remove(scene.children[i]); } //clear the checkboxes const myNode = document.getElementById("animationsPanel") as HTMLDivElement; while (myNode.firstChild) { myNode.removeChild(myNode.lastChild as any); } const axesHelper = new THREE.AxesHelper(5) scene.add(axesHelper) const light1 = new THREE.DirectionalLight(new THREE.Color(0xffcccc)); light1.position.set(-1, 1, 1) scene.add(light1); const light2 = new THREE.DirectionalLight(new THREE.Color(0xccffcc)); light2.position.set(1, 1, 1) scene.add(light2); const light3 = new THREE.DirectionalLight(new THREE.Color(0xccccff)); light3.position.set(0, -1, 0) scene.add(light3); const files = (evt.dataTransfer as DataTransfer).files const reader = new FileReader(); reader.onload = function () { gltfLoader.parse(reader.result as string, "/", (gltf: GLTF) => { console.log(gltf.scene) mixer = new THREE.AnimationMixer(gltf.scene); console.log(gltf.animations) if (gltf.animations.length > 0) { const animationsPanel: HTMLDivElement = document.getElementById("animationsPanel") as HTMLDivElement const ul: HTMLUListElement = document.createElement("UL") as HTMLUListElement const ulElem = animationsPanel.appendChild(ul) gltf.animations.forEach((a: THREE.AnimationClip, i) => { const li: HTMLLIElement = document.createElement("UL") as HTMLLIElement const liElem = ulElem.appendChild(li) const checkBox = document.createElement("INPUT") as HTMLInputElement checkBox.id = "checkbox_" + i checkBox.type = "checkbox" checkBox.addEventListener("change", (e: Event) => { if ((e.target as HTMLInputElement).checked) { mixer.clipAction((gltf as any).animations[i]).play() } else { mixer.clipAction((gltf as any).animations[i]).stop() } }) liElem.appendChild(checkBox) const label = document.createElement("LABEL") as HTMLLabelElement label.htmlFor = "checkbox_" + i label.innerHTML = a.name; liElem.appendChild(label) }) } else { const animationsPanel: HTMLDivElement = document.getElementById("animationsPanel") as HTMLDivElement animationsPanel.innerHTML = "No animations found in model" } scene.add(gltf.scene) const bbox = new THREE.Box3().setFromObject(gltf.scene); controls.target.x = ((bbox.min.x + bbox.max.x) / 2) controls.target.y = ((bbox.min.y + bbox.max.y) / 2) controls.target.z = ((bbox.min.z + bbox.max.z) / 2) modelReady = true }, (error) => { console.log(error); } ) } reader.readAsArrayBuffer(files[0]); }; 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) 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(); |
./dist/client/index.html
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 | <!DOCTYPE html> <html> <head> <title>Three.js TypeScript Tutorials by Sean Bradley</title> <style> body { overflow: hidden; margin: 0px; } #animationsPanel { position: absolute; top: 10px; right: 10px; width: 300px; height: auto; border: 1px solid white; color: white; font-family: monospace; font-size: 17px; } #animationsPanel ul { padding: 4px; list-style-type: none; } #dropzone { position: absolute; width: 140px; height: 90px; left: 10px; top: 50px; border: 10px dotted gray; background-color: lightgray; padding: 20px; font-family: monospace; font-size: 17px; } #dropzone p { pointer-events: none; } </style> </head> <body> <div id="dropzone"> <p>Drag (uncompressed) animated gLTF/gLB here</p> </div> <div id="animationsPanel"></div> <script type="module" src="client.js"></script> </body> </html> |