PropertyBinding.js 10.9 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

	this.rootNode = rootNode;
	this.trackName = trackName;
13 14
	this.referenceCount = 0;
	this.originalValue = null; // the value of the property before it was controlled by this binding
15

16
	var parseResults = THREE.PropertyBinding.parseTrackName( trackName );
17

B
Ben Houston 已提交
18
	//console.log( parseResults );
19
	this.directoryName = parseResults.directoryName;
20
	this.nodeName = parseResults.nodeName;
B
Ben Houston 已提交
21 22
	this.objectName = parseResults.objectName;
	this.objectIndex = parseResults.objectIndex;
23 24
	this.propertyName = parseResults.propertyName;
	this.propertyIndex = parseResults.propertyIndex;
25

26
	this.node = THREE.PropertyBinding.findNode( rootNode, this.nodeName );
27

28 29
	this.cumulativeValue = null;
	this.cumulativeWeight = 0;
30 31
};

32
THREE.PropertyBinding.prototype = {
33

34
	constructor: THREE.PropertyBinding,
35

36
	reset: function() {
37

38 39
		this.cumulativeValue = null;
		this.cumulativeWeight = 0;
40

41 42 43 44
	},

	accumulate: function( value, weight ) {
		
B
Ben Houston 已提交
45 46
		if( ! this.isBound ) this.bind();

47
		if( this.cumulativeWeight === 0 ) {
48

49 50
			if( this.cumulativeValue === null ) {
				this.cumulativeValue = THREE.AnimationUtils.clone( value );
51
			}
52 53 54 55 56
			this.cumulativeWeight = weight;
			//console.log( this );

		}
		else {
57

58
			var lerpAlpha = weight / ( this.cumulativeWeight + weight );
B
Ben Houston 已提交
59
			this.cumulativeValue = this.lerpValue( this.cumulativeValue, value, lerpAlpha );
60 61
			this.cumulativeWeight += weight;
			//console.log( this );
62

63 64
		}

65 66 67 68
	},

	unbind: function() {

B
Ben Houston 已提交
69
		if( ! this.isBound ) return;
70

B
Ben Houston 已提交
71 72 73 74 75 76 77 78
		this.setValue( this.originalValue );

		this.setValue = null;
		this.getValue = null;
		this.lerpValue = null;
		this.equalsValue = null;
		this.triggerDirty = null;	
		this.isBound = false;
79

80
	},
81

82 83 84 85
	// creates the member functions:
	//	- setValue( value )
	//  - getValue()
	//  - triggerDirty()
86

87
	bind: function() {
88

B
Ben Houston 已提交
89
		if( this.isBound ) return;
90 91
		
		//console.log( "PropertyBinding", this );
92

93
		var targetObject = this.node;
94

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

101 102 103 104 105 106
		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;				
107
				}
108 109 110
				if( ! targetObject.material.materials ) {
					console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
					return;				
111
				}
112 113 114 115 116
				targetObject = targetObject.material.materials;
			}
			else if( this.objectName === "bones" ) {
				if( ! targetObject.skeleton ) {
					console.error( '  can not bind to bones as node does not have a skeleton', this );
117
				}
118 119 120
				targetObject = targetObject.skeleton.bones;

				// TODO/OPTIMIZE, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
121
				
122 123 124 125 126 127 128
				// 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;
129
					}
130
				}
131 132
			}
			else {
133

134 135 136 137 138
				if( targetObject[ this.objectName ] === undefined ) {
					console.error( '  can not bind to objectName of node, undefined', this );			
					return;
				}
				targetObject = targetObject[ this.objectName ];
139
			}
140 141 142 143 144 145
			
			if( this.objectIndex !== undefined ) {
				if( targetObject[ this.objectIndex ] === undefined ) {
					console.error( "  trying to bind to objectIndex of objectName, but is undefined:", this, targetObject );
					return;				
				}
146

147
				targetObject = targetObject[ this.objectIndex ];
148 149
			}

150
		}
151

152 153 154 155 156 157
 		// 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;
		}
158

159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
		// access a sub element of the property array (only primitives are supported right now)
		if( this.propertyIndex !== undefined ) {

			if( this.propertyName === "morphTargetInfluences" ) {
				// 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 morphTargetInfluence name: ", this.propertyIndex );
				if( ! this.node.geometry ) {
					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry', this );				
				}
				if( ! this.node.geometry.morphTargets ) {
					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 );
						this.propertyIndex = i;
						break;
179
					}
180 181
				}
			}
182

183 184
			//console.log( '  update property array ' + this.propertyName + '[' + this.propertyIndex + '] via assignment.' );				
			this.setValue = function( value ) {
B
Ben Houston 已提交
185
				if( ! this.equalsValue( nodeProperty[ this.propertyIndex ], value ) ) {
186 187
					nodeProperty[ this.propertyIndex ] = value;
					return true;
188
				}
189 190 191 192 193 194
				return false;
			};

			this.getValue = function() {
				return nodeProperty[ this.propertyIndex ];
			};
195

196 197 198 199 200 201
		}
		// must use copy for Object3D.Euler/Quaternion		
		else if( nodeProperty.copy ) {
			
			//console.log( '  update property ' + this.name + '.' + this.propertyName + ' via a set() function.' );				
			this.setValue = function( value ) {
B
Ben Houston 已提交
202
				if( ! this.equalsValue( nodeProperty, value ) ) {
203 204
					nodeProperty.copy( value );
					return true;
205
				}
206 207 208 209 210 211 212 213 214 215 216 217 218
				return false;
			}

			this.getValue = function() {
				return nodeProperty;
			};

		}
		// otherwise just set the property directly on the node (do not use nodeProperty as it may not be a reference object)
		else {

			//console.log( '  update property ' + this.name + '.' + this.propertyName + ' via assignment.' );				
			this.setValue = function( value ) {
B
Ben Houston 已提交
219
				if( ! this.equalsValue( targetObject[ this.propertyName ], value ) ) {
220 221
					targetObject[ this.propertyName ] = value;	
					return true;
222
				}
223
				return false;
224
			}
225

226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
			this.getValue = function() {
				return targetObject[ this.propertyName ];
			};

		}

		// 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;
			}

		}

		this.originalValue = this.getValue();

B
Ben Houston 已提交
252 253 254 255
		this.equalsValue = THREE.AnimationUtils.getEqualsFunc( this.originalValue );
		this.lerpValue = THREE.AnimationUtils.getLerpFunc( this.originalValue, true );

		this.isBound = true;
256 257 258 259 260 261

	},

	apply: function() {

		// for speed capture the setter pattern as a closure (sort of a memoization pattern: https://en.wikipedia.org/wiki/Memoization)
B
Ben Houston 已提交
262
		if( ! this.isBound ) this.bind();
263

264
		// early exit if there is nothing to apply.
B
Ben Houston 已提交
265 266
		if( this.cumulativeWeight > 0 ) {
		
267 268 269 270 271
			// blend with original value
			if( this.cumulativeWeight < 1 ) {

				var remainingWeight = 1 - this.cumulativeWeight;
				var lerpAlpha = remainingWeight / ( this.cumulativeWeight + remainingWeight );
B
Ben Houston 已提交
272
				this.cumulativeValue = this.lerpValueler( this.cumulativeValue, this.originalValue, lerpAlpha );
273 274 275 276

			}

			var valueChanged = this.setValue( this.cumulativeValue );
277

B
Ben Houston 已提交
278 279 280
			if( valueChanged && this.triggerDirty ) {
				this.triggerDirty();
			}
281

282 283 284
			// reset accumulator
			this.cumulativeValue = null;
			this.cumulativeWeight = 0;
B
Ben Houston 已提交
285

286
		}
287 288 289 290 291
	}

};


292
THREE.PropertyBinding.parseTrackName = function( trackName ) {
293 294

	// matches strings in the form of:
295 296
	//    nodeName.property
	//    nodeName.property[accessor]
297
	//    nodeName.material.property[accessor]
298
	//    uuid.property[accessor]
299
	//    uuid.objectName[objectIndex].propertyName[propertyIndex]
300 301
	//    parentName/nodeName.property
	//    parentName/parentName/nodeName.property[index]
302
	//	  .bone[Armature.DEF_cog].position
303 304
	// created and tested via https://regex101.com/#javascript

305
	var re = /^(([\w]+\/)*)([\w-\d]+)?(\.([\w]+)(\[([\w\d\[\]\_. ]+)\])?)?(\.([\w.]+)(\[([\w\d\[\]\_. ]+)\])?)$/; 
306 307 308 309 310 311 312 313 314 315
	var matches = re.exec(trackName);

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

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

316
	var results = {
317
		directoryName: matches[1],
B
Ben Houston 已提交
318
		nodeName: matches[3], 	// allowed to be null, specified root node.
319 320 321 322
		objectName: matches[5],
		objectIndex: matches[7],
		propertyName: matches[9],
		propertyIndex: matches[11]	// allowed to be null, specifies that the whole property is set.
323 324
	};

325
	//console.log( "PropertyBinding.parseTrackName", trackName, results, matches );
326

327 328 329 330 331
	if( results.propertyName === null || results.propertyName.length === 0 ) {
		throw new Error( "can not parse propertyName from trackName: " + trackName );
	}

	return results;
332 333 334 335

};

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

338
	//console.log( 'AnimationUtils.findNode( ' + root.name + ', nodeName: ' + nodeName + ')');
339
	
340
	if( ! nodeName || nodeName === "" || nodeName === "root" || nodeName === "." || nodeName === -1 || nodeName === root.name || nodeName === root.uuid ) {
341

342
		//console.log( '  root.' );
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
		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 已提交
358
					return bone;
359 360 361 362 363 364 365 366

				}
			}

			return null;

		};

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

B
Ben Houston 已提交
369
		if( bone ) {
370

371
			//console.log( '  bone: ' + bone.name + '.' );
B
Ben Houston 已提交
372
			return bone;
373 374 375 376 377 378 379 380 381 382 383 384 385

		}
	}

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

		var searchNodeSubtree = function( children ) {

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

				var childNode = children[i];

386
				if( childNode.name === nodeName || childNode.uuid === nodeName ) {
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405

					return childNode;

				}

				var result = searchNodeSubtree( childNode.children );

				if( result ) return result;

			}

			return null;	

		};

		var subTreeNode = searchNodeSubtree( root.children );

		if( subTreeNode ) {

406
			//console.log( '  node: ' + subTreeNode.name + '.' );
407 408 409 410 411 412
			return subTreeNode;

		}

	}

413
	//console.log( "   <null>.  No node found for name: " + nodeName );
414 415 416

	return null;
}