PropertyBinding.js 9.8 KB
Newer Older
1 2 3 4 5 6 7 8
/**
 *
 * A track bound to a real value in the scene graph.
 * 
 * @author Ben Houston / http://clara.io/
 * @author David Sarno / http://lighthaus.us/
 */

9
THREE.PropertyBinding = function ( rootNode, trackName ) {
10 11 12 13

	this.rootNode = rootNode;
	this.trackName = trackName;

14
	var parseResults = THREE.PropertyBinding.parseTrackName( trackName );
15

16 17
	console.log( parseResults );
	this.directoryName = parseResults.directoryName;
18
	this.nodeName = parseResults.nodeName;
19
	this.material = parseResults.material;
20
	this.materialIndex = parseResults.materialIndex;
21 22
	this.propertyName = parseResults.propertyName;
	this.propertyIndex = parseResults.propertyIndex;
23

24
	this.node = THREE.PropertyBinding.findNode( rootNode, this.nodeName );
25

26 27
	this.cumulativeValue = null;
	this.cumulativeWeight = 0;
28 29
};

30
THREE.PropertyBinding.prototype = {
31

32
	constructor: THREE.PropertyBinding,
33

34
	reset: function() {
35

36 37
		this.cumulativeValue = null;
		this.cumulativeWeight = 0;
38

39 40 41 42
	},

	accumulate: function( value, weight ) {
		
43
		var lerp = THREE.AnimationUtils.getLerpFunc( value, true );
44

45
		this.accumulate = function( value, weight ) {
46

47 48 49 50 51 52 53
			if( this.cumulativeWeight === 0 ) {

				this.cumulativeValue = value;
				this.cumulativeWeight = weight;

			}
			else {
54

55 56 57
				var lerpAlpha = weight / ( this.cumulativeWeight + weight );
				this.cumulativeValue = lerp( this.cumulativeValue, value, lerpAlpha );
				this.cumulativeWeight += weight;
58

59
			}
60 61
		}

62
		this.accumulate( value, weight );
63

64
	},
65 66 67

	apply: function() {

B
Ben Houston 已提交
68
		// for speed capture the setter pattern as a closure (sort of a memoization pattern: https://en.wikipedia.org/wiki/Memoization)
69 70 71 72
		if( ! this.internalApply ) {

			 //console.log( "PropertyBinding.set( " + value + ")" );

73 74
			 var equalsFunc = THREE.AnimationUtils.getEqualsFunc( this.cumulativeValue );

75 76 77 78 79 80 81 82
			 var targetObject = this.node;

	 		// ensure there is a value node
			if( ! targetObject ) {
				console.error( "  trying to update node for track: " + this.trackName + " but it wasn't found." );
				return;
			}

83 84 85 86 87 88
			if( this.objectName ) {
				// special case were we need to reach deeper into the hierarchy to get the face materials....
				if( this.objectName === "materials" ) {
					if( ! targetObject.material ) {
						console.error( '  can not bind to material as node does not have a material', this );
						return;				
89
					}
90 91
					if( ! targetObject.material.materials ) {
						console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
92
						return;				
93
					}
94
					targetObject = targetObject.material.materials;
95
				}
96 97 98 99 100
				else if( this.objectName === "bones" ) {
					if( ! targetObject.skeleton ) {
						console.error( '  can not bind to bones as node does not have a skeleton', this );
					}
					targetObject = targetObject.skeleton.bones;
101

102 103 104 105 106 107 108 109 110 111 112
					// TODO/OPTIMIZE, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
					
					// support resolving morphTarget names into indices.
					console.log( "  resolving bone name: ", this.objectIndex );
					for( var i = 0; i < this.node.skeleton.bones.length; i ++ ) {
						if( this.node.skeleton.bones[i].name === this.objectIndex ) {
							console.log( "  resolved to index: ", i );
							this.objectIndex = i;
							break;
						}
					}
113
				}
114 115 116 117 118 119 120
				else {

					if( targetObject[ this.objectName ] === undefined ) {
						console.error( '  can not bind to objectName of node, undefined', this );			
						return;
					}
					targetObject = targetObject[ this.objectName ];
121
				}
122 123 124 125 126 127 128 129
				
				if( this.objectIndex !== undefined ) {
					if( targetObject[ this.objectIndex ] === undefined ) {
						console.error( "  trying to bind to objectIndex of objectName, but is undefined:", this, targetObject );
						return;				
					}

					targetObject = targetObject[ this.objectIndex ];
130
				}
131

132
			}
133 134 135 136 137 138

	 		// special case mappings
	 		var nodeProperty = targetObject[ this.propertyName ];
			if( ! nodeProperty ) {
				console.error( "  trying to update property for track: " + this.nodeName + '.' + this.propertyName + " but it wasn't found.", targetObject );				
				return;
139 140 141 142
			}

			// access a sub element of the property array (only primitives are supported right now)
			if( this.propertyIndex !== undefined ) {
143 144

				if( this.propertyName === "morphTargetInfluences" ) {
145 146
					// TODO/OPTIMIZE, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
					
147
					// support resolving morphTarget names into indices.
148
					//console.log( "  resolving morphTargetInfluence name: ", this.propertyIndex );
B
Ben Houston 已提交
149
					if( ! this.node.geometry ) {
150 151
						console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry', this );				
					}
B
Ben Houston 已提交
152
					if( ! this.node.geometry.morphTargets ) {
153 154 155 156 157 158
						console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry.morphTargets', this );				
					}
					
					for( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
						if( this.node.geometry.morphTargets[i].name === this.propertyIndex ) {
							//console.log( "  resolved to index: ", i );
159 160 161 162 163
							this.propertyIndex = i;
							break;
						}
					}
				}
164

165 166
				//console.log( '  update property array ' + this.propertyName + '[' + this.propertyIndex + '] via assignment.' );				
				this.internalApply = function() {
167 168 169 170 171
					if( ! equalsFunc( nodeProperty[ this.propertyIndex ], this.cumulativeValue ) ) {
						nodeProperty[ this.propertyIndex ] = this.cumulativeValue;
						return true;
					}
					return false;
172 173 174 175 176 177
				};
			}
			// must use copy for Object3D.Euler/Quaternion		
			else if( nodeProperty.copy ) {
				//console.log( '  update property ' + this.name + '.' + this.propertyName + ' via a set() function.' );				
				this.internalApply = function() {
178 179 180 181 182
					if( ! equalsFunc( nodeProperty, this.cumulativeValue ) ) {
						nodeProperty.copy( this.cumulativeValue );
						return true;
					}
					return false;
183 184 185 186
				}
			}
			// otherwise just set the property directly on the node (do not use nodeProperty as it may not be a reference object)
			else {
187

188 189
				//console.log( '  update property ' + this.name + '.' + this.propertyName + ' via assignment.' );				
				this.internalApply = function() {
190 191 192 193 194
					if( ! equalsFunc( targetObject[ this.propertyName ], this.cumulativeValue ) ) {
						targetObject[ this.propertyName ] = this.cumulativeValue;	
						return true;
					}
					return false;
195 196
				}
			}
197

198 199 200 201 202 203 204 205 206 207 208 209 210
			// trigger node dirty			
			if( targetObject.needsUpdate !== undefined ) { // material
				//console.log( '  triggering material as dirty' );
				this.triggerDirty = function() {
					this.node.needsUpdate = true;
				}
			}			
			else if( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
				//console.log( '  triggering node as dirty' );
				this.triggerDirty = function() {
					targetObject.matrixWorldNeedsUpdate = true;
				}
			}
211 212 213

		}

214 215 216
		// early exit if there is nothing to apply.
		if( this.cumulativeWeight <= 0 ) {
			return;
217 218
		}

219
		var valueChanged = this.internalApply();
220

221
		if( valueChanged && this.triggerDirty ) {
B
Ben Houston 已提交
222 223
			this.triggerDirty();
		}
224 225 226 227 228 229 230 231 232 233 234
	},

	get: function() {

		throw new Error( "TODO" );

	}

};


235
THREE.PropertyBinding.parseTrackName = function( trackName ) {
236 237

	// matches strings in the form of:
238 239
	//    nodeName.property
	//    nodeName.property[accessor]
240
	//    nodeName.material.property[accessor]
241
	//    uuid.property[accessor]
242
	//    uuid.objectName[objectIndex].propertyName[propertyIndex]
243 244
	//    parentName/nodeName.property
	//    parentName/parentName/nodeName.property[index]
245
	//	  .bone[Armature.DEF_cog].position
246 247
	// created and tested via https://regex101.com/#javascript

248
	var re = /^(([\w]+\/)*)([\w-]+)?(\.([\w]+)(\[([\w.]+)\])?)?(\.([\w.]+)(\[([\w]+)\])?)$/; 
249 250 251 252 253 254 255 256 257 258
	var matches = re.exec(trackName);

	if( ! matches ) {
		throw new Error( "cannot parse trackName at all: " + trackName );
	}

    if (matches.index === re.lastIndex) {
        re.lastIndex++;
    }

259
	var results = {
260
		directoryName: matches[1],
B
Ben Houston 已提交
261
		nodeName: matches[3], 	// allowed to be null, specified root node.
262 263 264 265
		objectName: matches[5],
		objectIndex: matches[7],
		propertyName: matches[9],
		propertyIndex: matches[11]	// allowed to be null, specifies that the whole property is set.
266 267
	};

268
	//console.log( "PropertyBinding.parseTrackName", trackName, results, matches );
269

270 271 272 273 274
	if( results.propertyName === null || results.propertyName.length === 0 ) {
		throw new Error( "can not parse propertyName from trackName: " + trackName );
	}

	return results;
275 276 277 278

};

// TODO: Cache this at some point
279
THREE.PropertyBinding.findNode = function( root, nodeName ) {
280

281
	//console.log( 'AnimationUtils.findNode( ' + root.name + ', nodeName: ' + nodeName + ')');
282
	
283
	if( ! nodeName || nodeName === "" || nodeName === "root" || nodeName === "." || nodeName === -1 || nodeName === root.name || nodeName === root.uuid ) {
284

285
		//console.log( '  root.' );
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
		return root;

	}

	// (2) search into skeleton bones.
	if( root.skeleton ) {

		var searchSkeleton = function( skeleton ) {

			for( var i = 0; i < skeleton.bones.length; i ++ ) {

				var bone = skeleton.bones[i];

				if( bone.name === nodeName ) {

B
Ben Houston 已提交
301
					return bone;
302 303 304 305 306 307 308 309

				}
			}

			return null;

		};

B
Ben Houston 已提交
310
		var bone = searchSkeleton( root.skeleton );
311

B
Ben Houston 已提交
312
		if( bone ) {
313

314
			//console.log( '  bone: ' + bone.name + '.' );
B
Ben Houston 已提交
315
			return bone;
316 317 318 319 320 321 322 323 324 325 326 327 328

		}
	}

	// (3) search into node subtree.
	if( root.children ) {

		var searchNodeSubtree = function( children ) {

			for( var i = 0; i < children.length; i ++ ) {

				var childNode = children[i];

329
				if( childNode.name === nodeName || childNode.uuid === nodeName ) {
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348

					return childNode;

				}

				var result = searchNodeSubtree( childNode.children );

				if( result ) return result;

			}

			return null;	

		};

		var subTreeNode = searchNodeSubtree( root.children );

		if( subTreeNode ) {

349
			//console.log( '  node: ' + subTreeNode.name + '.' );
350 351 352 353 354 355
			return subTreeNode;

		}

	}

356
	//console.log( "   <null>.  No node found for name: " + nodeName );
357 358 359

	return null;
}