├── .gitignore ├── README.md ├── example ├── images │ └── screencapture.gif ├── index.html └── models │ ├── capoeira-serialized.json │ └── capoeira.json ├── package.json ├── parseAnimation.js └── serializeAnimation.js /.gitignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # threejs-serialize-animation 2 | A script that serializes animations in a Three.js (json) file and makes it smaller. 3 | 4 | [Example](https://brunoimbrizi.github.io/threejs-serialize-animation/example/) 5 | 6 | ![Sample](https://raw.githubusercontent.com/brunoimbrizi/threejs-serialize-animation/master/example/images/screencapture.gif) 7 | original 1,148 KB / serialized 475 KB 8 | 9 | ## Why 10 | Three.js (json) files containing animations can get quite big. Using the Blender Exporter it is possible to reduce file sizes by turning 'Indent JSON' off, or turning 'Enable precision' on or setting 'File compression' to msgpack. 11 | 12 | But there is a way to get even smaller files: to serialize the key values in the `animations` node. 13 | 14 | The animation that promped me to write this script contained a skeleton with 23 joints and was 6214 frames long. Discarding the bones and the animations, the exported json would be around 200 KB, but with the animations it was 28,484 KB. 15 | By serializing the animations the size went down to 12,164 KB. 16 | 17 | Some test results with different settings: 18 | 19 | * 28,484 kb - precision 6, indent true 20 | * 16,854 kb - precision 6, indent false 21 | * 12,017 kb - precision 3, indent false 22 | * 16,968 kb - msgpack compression 23 | * 7,298 kb - precision 3, serialized 24 | 25 | ## What 26 | `serializeAnimation.js` copies the values of each keyframe property into a single array. 27 | 28 | i.e. 29 | ``` 30 | "keys":[{ 31 | "time": 0, 32 | "scl": [1, 1, 1], 33 | "pos": [0, 0, 0], 34 | "rot": [1, 2, 3, 4] 35 | },{ 36 | "time": 1, 37 | "scl": [1, 1, 1], 38 | "pos": [0, 0, 0], 39 | "rot": [5, 6, 7, 8] 40 | } 41 | ``` 42 | 43 | Becomes: 44 | ``` 45 | "keys":[{ 46 | "time": [0, 1] 47 | "scl": [1, 1, 1, 1, 1, 1], 48 | "pos": [0, 0, 0, 0, 0, 0], 49 | "rot": [1, 2, 3, 4, 5, 6, 7, 8] 50 | } 51 | ``` 52 | 53 | ## It's a hack 54 | Three.js is not expecting a single key containing all the values. It is necessary to hack it a bit, namely by overriding `AnimationClip.parseAnimation`. This is what `parseAnimation.js` is doing. 55 | 56 | What is curious is that internally the values are already serialized. This happens inside `AnimationClip.parseAnimation`, more precisely when `flattenJSON` is called. 57 | 58 | What `parseAnimation.js` does it just to bypass that call and just use the values already serialized in the file. 59 | 60 | ## How 61 | 62 | #### Serialize 63 | ``` 64 | $ node serializeAnimation.js 65 | ``` 66 | // output input-file-serialized.json 67 | 68 | #### Parse 69 | ``` 70 | import * as THREE from 'three'; 71 | import { parseAnimation } from 'threejs-serialize-animation'; 72 | 73 | // override 74 | THREE.AnimationClip.parseAnimation = parseAnimation; 75 | ``` 76 | -------------------------------------------------------------------------------- /example/images/screencapture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunoimbrizi/threejs-serialize-animation/8551ad2d386c1ca19061c1adf1f95084d4d67788/example/images/screencapture.gif -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | three-serialize-animation 5 | 6 | 7 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 | capoeira animation from mixamo
43 | low poly male by sea206 44 |
45 | 46 | 47 | 48 | 49 | 50 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threejs-serialize-animation", 3 | "version": "1.0.0", 4 | "description": "A script that serializes animations in a Three.js (json) file and makes it smaller.", 5 | "main": "parseAnimation.js", 6 | "scripts": { }, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/brunoimbrizi/threejs-serialize-animation.git" 10 | }, 11 | "keywords": [ 12 | "threejs", 13 | "skeleton", 14 | "bones", 15 | "animation" 16 | ], 17 | "author": "Bruno Imbrizi (http://brunoimbrizi.com)", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/brunoimbrizi/threejs-serialize-animation/issues" 21 | }, 22 | "homepage": "https://github.com/brunoimbrizi/threejs-serialize-animation#readme" 23 | } -------------------------------------------------------------------------------- /parseAnimation.js: -------------------------------------------------------------------------------- 1 | var THREE = window.THREE || require('three'); 2 | 3 | var parseAnimation = function ( animation, bones ) { 4 | 5 | if ( ! animation ) { 6 | 7 | console.error( 'THREE.AnimationClip: No animation in JSONLoader data.' ); 8 | return null; 9 | 10 | } 11 | 12 | var addNonemptyTrack = function ( trackType, trackName, animationKeys, propertyName, destTracks ) { 13 | 14 | // only return track if there are actually keys. 15 | if ( animationKeys.length !== 0 ) { 16 | 17 | var times = []; 18 | var values = []; 19 | 20 | // COMMENTED OUT LINE BELOW FROM ORIGINAL THREE.AnimationClip 21 | // AnimationUtils.flattenJSON( animationKeys, times, values, propertyName ); 22 | 23 | // AND ADDED THE NEXT TWO LINES 24 | times = animationKeys[0].time; 25 | values = animationKeys[0][propertyName]; 26 | 27 | // empty keys are filtered out, so check again 28 | if ( times.length !== 0 ) { 29 | 30 | destTracks.push( new trackType( trackName, times, values ) ); 31 | 32 | } 33 | 34 | } 35 | 36 | }; 37 | 38 | var tracks = []; 39 | 40 | var clipName = animation.name || 'default'; 41 | // automatic length determination in AnimationClip. 42 | var duration = animation.length || - 1; 43 | var fps = animation.fps || 30; 44 | 45 | var hierarchyTracks = animation.hierarchy || []; 46 | 47 | for ( var h = 0; h < hierarchyTracks.length; h ++ ) { 48 | 49 | var animationKeys = hierarchyTracks[ h ].keys; 50 | 51 | // skip empty tracks 52 | if ( ! animationKeys || animationKeys.length === 0 ) continue; 53 | 54 | // process morph targets 55 | if ( animationKeys[ 0 ].morphTargets ) { 56 | 57 | // figure out all morph targets used in this track 58 | var morphTargetNames = {}; 59 | 60 | for ( var k = 0; k < animationKeys.length; k ++ ) { 61 | 62 | if ( animationKeys[ k ].morphTargets ) { 63 | 64 | for ( var m = 0; m < animationKeys[ k ].morphTargets.length; m ++ ) { 65 | 66 | morphTargetNames[ animationKeys[ k ].morphTargets[ m ] ] = - 1; 67 | 68 | } 69 | 70 | } 71 | 72 | } 73 | 74 | // create a track for each morph target with all zero 75 | // morphTargetInfluences except for the keys in which 76 | // the morphTarget is named. 77 | for ( var morphTargetName in morphTargetNames ) { 78 | 79 | var times = []; 80 | var values = []; 81 | 82 | for ( var m = 0; m !== animationKeys[ k ].morphTargets.length; ++ m ) { 83 | 84 | var animationKey = animationKeys[ k ]; 85 | 86 | times.push( animationKey.time ); 87 | values.push( ( animationKey.morphTarget === morphTargetName ) ? 1 : 0 ); 88 | 89 | } 90 | 91 | tracks.push( new NumberKeyframeTrack( '.morphTargetInfluence[' + morphTargetName + ']', times, values ) ); 92 | 93 | } 94 | 95 | duration = morphTargetNames.length * ( fps || 1.0 ); 96 | 97 | } else { 98 | 99 | // ...assume skeletal animation 100 | 101 | var boneName = '.bones[' + bones[ h ].name + ']'; 102 | 103 | addNonemptyTrack( 104 | THREE.VectorKeyframeTrack, boneName + '.position', 105 | animationKeys, 'pos', tracks ); 106 | 107 | addNonemptyTrack( 108 | THREE.QuaternionKeyframeTrack, boneName + '.quaternion', 109 | animationKeys, 'rot', tracks ); 110 | 111 | addNonemptyTrack( 112 | THREE.VectorKeyframeTrack, boneName + '.scale', 113 | animationKeys, 'scl', tracks ); 114 | 115 | } 116 | 117 | } 118 | 119 | if ( tracks.length === 0 ) { 120 | 121 | return null; 122 | 123 | } 124 | 125 | var clip = new THREE.AnimationClip( clipName, duration, tracks ); 126 | 127 | return clip; 128 | 129 | }; 130 | 131 | window.module = window.module || {}; 132 | module.exports = { parseAnimation: parseAnimation }; 133 | -------------------------------------------------------------------------------- /serializeAnimation.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const args = process.argv.slice(2); 4 | const input = args[0]; 5 | const indexDot = input.lastIndexOf('.'); 6 | const fileName = (indexDot > -1) ? input.substr(0, indexDot) : input; 7 | const fileExt = (indexDot > -1) ? input.substr(indexDot) : '.json'; 8 | 9 | fs.readFile(input, 'utf8', function (err, data) { 10 | if (err) return console.log(err); 11 | 12 | const serialized = getSerializedAnimations(data); 13 | const output = `${fileName}-serialized${fileExt}`; 14 | 15 | fs.writeFile(output, serialized, (err) => { 16 | if (err) throw err; 17 | console.log(`Saved as ${output}`); 18 | }); 19 | }); 20 | 21 | const getSerializedAnimations = (input) => { 22 | const json = JSON.parse(input); 23 | 24 | const obj = {}; 25 | obj.animations = []; 26 | if (json.bones) obj.bones = json.bones; 27 | if (json.colors) obj.colors = json.colors; 28 | if (json.faces) obj.faces = json.faces; 29 | if (json.influencesPerVertex) obj.influencesPerVertex = json.influencesPerVertex; 30 | if (json.metadata) obj.metadata = json.metadata; 31 | if (json.morphTargets) obj.morphTargets = json.morphTargets; 32 | if (json.normals) obj.normals = json.normals; 33 | if (json.skinIndices) obj.skinIndices = json.skinIndices; 34 | if (json.skinWeights) obj.skinWeights = json.skinWeights; 35 | if (json.uvs) obj.uvs = json.uvs; 36 | if (json.vertices) obj.vertices = json.vertices; 37 | 38 | let aLength = 0; 39 | if (json.animations) aLength = json.animations.length; 40 | else if (json.animation) aLength = 1; 41 | 42 | for (let i = 0; i < aLength; i++) { 43 | const animation = json.animations ? json.animations[i] : json.animation; 44 | 45 | const nAnimation = {}; 46 | nAnimation.length = animation.length; 47 | nAnimation.name = animation.name; 48 | nAnimation.fps = animation.fps; 49 | nAnimation.hierarchy = []; 50 | 51 | for (let j = 0; j < animation.hierarchy.length; j++) { 52 | const hierarchy = animation.hierarchy[j]; 53 | const length = hierarchy.keys.length; 54 | 55 | const time = new Array(length * 1); 56 | const scl = new Array(length * 3); 57 | const pos = new Array(length * 3); 58 | const rot = new Array(length * 4); 59 | 60 | for (let k = 0; k < length; k++) { 61 | const key = hierarchy.keys[k]; 62 | 63 | time[k] = key.time; 64 | 65 | scl[k * 3 + 0] = key.scl ? key.scl[0] : scl[(k - 1) * 3 + 0]; 66 | scl[k * 3 + 1] = key.scl ? key.scl[1] : scl[(k - 1) * 3 + 1]; 67 | scl[k * 3 + 2] = key.scl ? key.scl[2] : scl[(k - 1) * 3 + 2]; 68 | 69 | pos[k * 3 + 0] = key.pos ? key.pos[0] : pos[(k - 1) * 3 + 0]; 70 | pos[k * 3 + 1] = key.pos ? key.pos[1] : pos[(k - 1) * 3 + 1]; 71 | pos[k * 3 + 2] = key.pos ? key.pos[2] : pos[(k - 1) * 3 + 2]; 72 | 73 | rot[k * 4 + 0] = key.rot ? key.rot[0] : rot[(k - 1) * 4 + 0]; 74 | rot[k * 4 + 1] = key.rot ? key.rot[1] : rot[(k - 1) * 4 + 1]; 75 | rot[k * 4 + 2] = key.rot ? key.rot[2] : rot[(k - 1) * 4 + 2]; 76 | rot[k * 4 + 3] = key.rot ? key.rot[3] : rot[(k - 1) * 4 + 3]; 77 | } 78 | 79 | const nHierarchy = {}; 80 | nHierarchy.parent = hierarchy.parent; 81 | nHierarchy.keys = []; 82 | 83 | const nKeys = {}; 84 | nKeys.time = time; 85 | nKeys.scl = scl; 86 | nKeys.pos = pos; 87 | nKeys.rot = rot; 88 | nHierarchy.keys.push(nKeys); 89 | 90 | nAnimation.hierarchy.push(nHierarchy); 91 | } 92 | 93 | obj.animations.push(nAnimation); 94 | } 95 | 96 | return JSON.stringify(obj); 97 | }; 98 | --------------------------------------------------------------------------------