PropertyBinding.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. /**
  2. *
  3. * A track bound to a real value in the scene graph.
  4. *
  5. * @author Ben Houston / http://clara.io/
  6. * @author David Sarno / http://lighthaus.us/
  7. */
  8. THREE.PropertyBinding = function ( rootNode, trackName ) {
  9. this.rootNode = rootNode;
  10. this.trackName = trackName;
  11. this.referenceCount = 0;
  12. this.originalValue = null; // the value of the property before it was controlled by this binding
  13. var parseResults = THREE.PropertyBinding.parseTrackName( trackName );
  14. this.directoryName = parseResults.directoryName;
  15. this.nodeName = parseResults.nodeName;
  16. this.objectName = parseResults.objectName;
  17. this.objectIndex = parseResults.objectIndex;
  18. this.propertyName = parseResults.propertyName;
  19. this.propertyIndex = parseResults.propertyIndex;
  20. this.node = THREE.PropertyBinding.findNode( rootNode, this.nodeName ) || rootNode;
  21. this.cumulativeValue = null;
  22. this.cumulativeWeight = 0;
  23. };
  24. THREE.PropertyBinding.prototype = {
  25. constructor: THREE.PropertyBinding,
  26. reset: function() {
  27. this.cumulativeValue = null;
  28. this.cumulativeWeight = 0;
  29. },
  30. accumulate: function( value, weight ) {
  31. if ( ! this.isBound ) this.bind();
  32. if ( this.cumulativeWeight === 0 ) {
  33. if ( weight > 0 ) {
  34. if ( this.cumulativeValue === null ) {
  35. this.cumulativeValue = THREE.AnimationUtils.clone( value );
  36. }
  37. this.cumulativeWeight = weight;
  38. }
  39. } else {
  40. var lerpAlpha = weight / ( this.cumulativeWeight + weight );
  41. this.cumulativeValue = this.lerpValue( this.cumulativeValue, value, lerpAlpha );
  42. this.cumulativeWeight += weight;
  43. }
  44. },
  45. unbind: function() {
  46. if ( ! this.isBound ) return;
  47. this.setValue( this.originalValue );
  48. this.setValue = null;
  49. this.getValue = null;
  50. this.lerpValue = null;
  51. this.equalsValue = null;
  52. this.triggerDirty = null;
  53. this.isBound = false;
  54. },
  55. // bind to the real property in the scene graph, remember original value, memorize various accessors for speed/inefficiency
  56. bind: function() {
  57. if ( this.isBound ) return;
  58. var targetObject = this.node;
  59. // ensure there is a value node
  60. if ( ! targetObject ) {
  61. console.error( " trying to update node for track: " + this.trackName + " but it wasn't found." );
  62. return;
  63. }
  64. if ( this.objectName ) {
  65. // special case were we need to reach deeper into the hierarchy to get the face materials....
  66. if ( this.objectName === "materials" ) {
  67. if ( ! targetObject.material ) {
  68. console.error( ' can not bind to material as node does not have a material', this );
  69. return;
  70. }
  71. if ( ! targetObject.material.materials ) {
  72. console.error( ' can not bind to material.materials as node.material does not have a materials array', this );
  73. return;
  74. }
  75. targetObject = targetObject.material.materials;
  76. } else if ( this.objectName === "bones" ) {
  77. if ( ! targetObject.skeleton ) {
  78. console.error( ' can not bind to bones as node does not have a skeleton', this );
  79. return;
  80. }
  81. // potential future optimization: skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
  82. targetObject = targetObject.skeleton.bones;
  83. // support resolving morphTarget names into indices.
  84. for ( var i = 0; i < targetObject.length; i ++ ) {
  85. if ( targetObject[i].name === this.objectIndex ) {
  86. this.objectIndex = i;
  87. break;
  88. }
  89. }
  90. } else {
  91. if ( targetObject[ this.objectName ] === undefined ) {
  92. console.error( ' can not bind to objectName of node, undefined', this );
  93. return;
  94. }
  95. targetObject = targetObject[ this.objectName ];
  96. }
  97. if ( this.objectIndex !== undefined ) {
  98. if ( targetObject[ this.objectIndex ] === undefined ) {
  99. console.error( " trying to bind to objectIndex of objectName, but is undefined:", this, targetObject );
  100. return;
  101. }
  102. targetObject = targetObject[ this.objectIndex ];
  103. }
  104. }
  105. // special case mappings
  106. var nodeProperty = targetObject[ this.propertyName ];
  107. if ( ! nodeProperty ) {
  108. console.error( " trying to update property for track: " + this.nodeName + '.' + this.propertyName + " but it wasn't found.", targetObject );
  109. return;
  110. }
  111. // access a sub element of the property array (only primitives are supported right now)
  112. if ( this.propertyIndex !== undefined ) {
  113. if ( this.propertyName === "morphTargetInfluences" ) {
  114. // potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
  115. // support resolving morphTarget names into indices.
  116. if ( ! targetObject.geometry ) {
  117. console.error( ' can not bind to morphTargetInfluences becasuse node does not have a geometry', this );
  118. }
  119. if ( ! targetObject.geometry.morphTargets ) {
  120. console.error( ' can not bind to morphTargetInfluences becasuse node does not have a geometry.morphTargets', this );
  121. }
  122. for ( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
  123. if ( targetObject.geometry.morphTargets[i].name === this.propertyIndex ) {
  124. this.propertyIndex = i;
  125. break;
  126. }
  127. }
  128. }
  129. this.setValue = function setValue_propertyIndexed( value ) {
  130. if ( ! this.equalsValue( nodeProperty[ this.propertyIndex ], value ) ) {
  131. nodeProperty[ this.propertyIndex ] = value;
  132. return true;
  133. }
  134. return false;
  135. };
  136. this.getValue = function getValue_propertyIndexed() {
  137. return nodeProperty[ this.propertyIndex ];
  138. };
  139. }
  140. // must use copy for Object3D.Euler/Quaternion
  141. else if ( nodeProperty.copy ) {
  142. this.setValue = function setValue_propertyObject( value ) {
  143. if ( ! this.equalsValue( nodeProperty, value ) ) {
  144. nodeProperty.copy( value );
  145. return true;
  146. }
  147. return false;
  148. }
  149. this.getValue = function getValue_propertyObject() {
  150. return nodeProperty;
  151. };
  152. }
  153. // otherwise just set the property directly on the node (do not use nodeProperty as it may not be a reference object)
  154. else {
  155. this.setValue = function setValue_property( value ) {
  156. if ( ! this.equalsValue( targetObject[ this.propertyName ], value ) ) {
  157. targetObject[ this.propertyName ] = value;
  158. return true;
  159. }
  160. return false;
  161. }
  162. this.getValue = function getValue_property() {
  163. return targetObject[ this.propertyName ];
  164. };
  165. }
  166. // trigger node dirty
  167. if ( targetObject.needsUpdate !== undefined ) { // material
  168. this.triggerDirty = function triggerDirty_needsUpdate() {
  169. this.node.needsUpdate = true;
  170. }
  171. } else if ( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
  172. this.triggerDirty = function triggerDirty_matrixWorldNeedsUpdate() {
  173. targetObject.matrixWorldNeedsUpdate = true;
  174. }
  175. }
  176. this.originalValue = this.getValue();
  177. this.equalsValue = THREE.AnimationUtils.getEqualsFunc( this.originalValue );
  178. this.lerpValue = THREE.AnimationUtils.getLerpFunc( this.originalValue, true );
  179. this.isBound = true;
  180. },
  181. apply: function() {
  182. // for speed capture the setter pattern as a closure (sort of a memoization pattern: https://en.wikipedia.org/wiki/Memoization)
  183. if ( ! this.isBound ) this.bind();
  184. // early exit if there is nothing to apply.
  185. if ( this.cumulativeWeight > 0 ) {
  186. // blend with original value
  187. if ( this.cumulativeWeight < 1 ) {
  188. var remainingWeight = 1 - this.cumulativeWeight;
  189. var lerpAlpha = remainingWeight / ( this.cumulativeWeight + remainingWeight );
  190. this.cumulativeValue = this.lerpValue( this.cumulativeValue, this.originalValue, lerpAlpha );
  191. }
  192. var valueChanged = this.setValue( this.cumulativeValue );
  193. if ( valueChanged && this.triggerDirty ) {
  194. this.triggerDirty();
  195. }
  196. // reset accumulator
  197. this.cumulativeValue = null;
  198. this.cumulativeWeight = 0;
  199. }
  200. }
  201. };
  202. THREE.PropertyBinding.parseTrackName = function( trackName ) {
  203. // matches strings in the form of:
  204. // nodeName.property
  205. // nodeName.property[accessor]
  206. // nodeName.material.property[accessor]
  207. // uuid.property[accessor]
  208. // uuid.objectName[objectIndex].propertyName[propertyIndex]
  209. // parentName/nodeName.property
  210. // parentName/parentName/nodeName.property[index]
  211. // .bone[Armature.DEF_cog].position
  212. // created and tested via https://regex101.com/#javascript
  213. var re = /^(([\w]+\/)*)([\w-\d]+)?(\.([\w]+)(\[([\w\d\[\]\_. ]+)\])?)?(\.([\w.]+)(\[([\w\d\[\]\_. ]+)\])?)$/;
  214. var matches = re.exec(trackName);
  215. if ( ! matches ) {
  216. throw new Error( "cannot parse trackName at all: " + trackName );
  217. }
  218. if (matches.index === re.lastIndex) {
  219. re.lastIndex++;
  220. }
  221. var results = {
  222. directoryName: matches[1],
  223. nodeName: matches[3], // allowed to be null, specified root node.
  224. objectName: matches[5],
  225. objectIndex: matches[7],
  226. propertyName: matches[9],
  227. propertyIndex: matches[11] // allowed to be null, specifies that the whole property is set.
  228. };
  229. if ( results.propertyName === null || results.propertyName.length === 0 ) {
  230. throw new Error( "can not parse propertyName from trackName: " + trackName );
  231. }
  232. return results;
  233. };
  234. THREE.PropertyBinding.findNode = function( root, nodeName ) {
  235. function searchSkeleton( skeleton ) {
  236. for ( var i = 0; i < skeleton.bones.length; i ++ ) {
  237. var bone = skeleton.bones[i];
  238. if ( bone.name === nodeName ) {
  239. return bone;
  240. }
  241. }
  242. return null;
  243. }
  244. function searchNodeSubtree( children ) {
  245. for ( var i = 0; i < children.length; i ++ ) {
  246. var childNode = children[i];
  247. if ( childNode.name === nodeName || childNode.uuid === nodeName ) {
  248. return childNode;
  249. }
  250. var result = searchNodeSubtree( childNode.children );
  251. if ( result ) return result;
  252. }
  253. return null;
  254. }
  255. //
  256. if ( ! nodeName || nodeName === "" || nodeName === "root" || nodeName === "." || nodeName === -1 || nodeName === root.name || nodeName === root.uuid ) {
  257. return root;
  258. }
  259. // search into skeleton bones.
  260. if ( root.skeleton ) {
  261. var bone = searchSkeleton( root.skeleton );
  262. if ( bone ) {
  263. return bone;
  264. }
  265. }
  266. // search into node subtree.
  267. if ( root.children ) {
  268. var subTreeNode = searchNodeSubtree( root.children );
  269. if ( subTreeNode ) {
  270. return subTreeNode;
  271. }
  272. }
  273. return null;
  274. }