MD2Character.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import {
  2. AnimationMixer,
  3. Box3,
  4. Mesh,
  5. MeshLambertMaterial,
  6. Object3D,
  7. TextureLoader,
  8. UVMapping,
  9. SRGBColorSpace
  10. } from 'three';
  11. import { MD2Loader } from '../loaders/MD2Loader.js';
  12. /**
  13. * This class represents a management component for animated MD2
  14. * character assets.
  15. */
  16. class MD2Character {
  17. /**
  18. * Constructs a new MD2 character.
  19. */
  20. constructor() {
  21. /**
  22. * The mesh scale.
  23. *
  24. * @type {number}
  25. * @default 1
  26. */
  27. this.scale = 1;
  28. /**
  29. * The FPS
  30. *
  31. * @type {number}
  32. * @default 6
  33. */
  34. this.animationFPS = 6;
  35. /**
  36. * The root 3D object
  37. *
  38. * @type {Object3D}
  39. */
  40. this.root = new Object3D();
  41. /**
  42. * The body mesh.
  43. *
  44. * @type {?Mesh}
  45. * @default null
  46. */
  47. this.meshBody = null;
  48. /**
  49. * The weapon mesh.
  50. *
  51. * @type {?Mesh}
  52. * @default null
  53. */
  54. this.meshWeapon = null;
  55. /**
  56. * The body skins.
  57. *
  58. * @type {Array<Texture>}
  59. */
  60. this.skinsBody = [];
  61. /**
  62. * The weapon skins.
  63. *
  64. * @type {Array<Texture>}
  65. */
  66. this.skinsWeapon = [];
  67. /**
  68. * The weapon meshes.
  69. *
  70. * @type {Array<Mesh>}
  71. */
  72. this.weapons = [];
  73. /**
  74. * The name of the active animation clip.
  75. *
  76. * @type {?string}
  77. * @default null
  78. */
  79. this.activeAnimationClipName = null;
  80. /**
  81. * The animation mixer.
  82. *
  83. * @type {?AnimationMixer}
  84. * @default null
  85. */
  86. this.mixer = null;
  87. /**
  88. * The `onLoad` callback function.
  89. *
  90. * @type {Function}
  91. */
  92. this.onLoadComplete = function () {};
  93. // internal
  94. this.loadCounter = 0;
  95. }
  96. /**
  97. * Loads the character model for the given config.
  98. *
  99. * @param {Object} config - The config which defines the model and textures paths.
  100. */
  101. loadParts( config ) {
  102. const scope = this;
  103. function createPart( geometry, skinMap ) {
  104. const materialWireframe = new MeshLambertMaterial( { color: 0xffaa00, wireframe: true } );
  105. const materialTexture = new MeshLambertMaterial( { color: 0xffffff, wireframe: false, map: skinMap } );
  106. //
  107. const mesh = new Mesh( geometry, materialTexture );
  108. mesh.rotation.y = - Math.PI / 2;
  109. mesh.castShadow = true;
  110. mesh.receiveShadow = true;
  111. //
  112. mesh.materialTexture = materialTexture;
  113. mesh.materialWireframe = materialWireframe;
  114. return mesh;
  115. }
  116. function loadTextures( baseUrl, textureUrls ) {
  117. const textureLoader = new TextureLoader();
  118. const textures = [];
  119. for ( let i = 0; i < textureUrls.length; i ++ ) {
  120. textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
  121. textures[ i ].mapping = UVMapping;
  122. textures[ i ].name = textureUrls[ i ];
  123. textures[ i ].colorSpace = SRGBColorSpace;
  124. }
  125. return textures;
  126. }
  127. function checkLoadingComplete() {
  128. scope.loadCounter -= 1;
  129. if ( scope.loadCounter === 0 ) scope.onLoadComplete();
  130. }
  131. this.loadCounter = config.weapons.length * 2 + config.skins.length + 1;
  132. const weaponsTextures = [];
  133. for ( let i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
  134. // SKINS
  135. this.skinsBody = loadTextures( config.baseUrl + 'skins/', config.skins );
  136. this.skinsWeapon = loadTextures( config.baseUrl + 'skins/', weaponsTextures );
  137. // BODY
  138. const loader = new MD2Loader();
  139. loader.load( config.baseUrl + config.body, function ( geo ) {
  140. const boundingBox = new Box3();
  141. boundingBox.setFromBufferAttribute( geo.attributes.position );
  142. scope.root.position.y = - scope.scale * boundingBox.min.y;
  143. const mesh = createPart( geo, scope.skinsBody[ 0 ] );
  144. mesh.scale.set( scope.scale, scope.scale, scope.scale );
  145. scope.root.add( mesh );
  146. scope.meshBody = mesh;
  147. scope.meshBody.clipOffset = 0;
  148. scope.activeAnimationClipName = mesh.geometry.animations[ 0 ].name;
  149. scope.mixer = new AnimationMixer( mesh );
  150. checkLoadingComplete();
  151. } );
  152. // WEAPONS
  153. const generateCallback = function ( index, name ) {
  154. return function ( geo ) {
  155. const mesh = createPart( geo, scope.skinsWeapon[ index ] );
  156. mesh.scale.set( scope.scale, scope.scale, scope.scale );
  157. mesh.visible = false;
  158. mesh.name = name;
  159. scope.root.add( mesh );
  160. scope.weapons[ index ] = mesh;
  161. scope.meshWeapon = mesh;
  162. checkLoadingComplete();
  163. };
  164. };
  165. for ( let i = 0; i < config.weapons.length; i ++ ) {
  166. loader.load( config.baseUrl + config.weapons[ i ][ 0 ], generateCallback( i, config.weapons[ i ][ 0 ] ) );
  167. }
  168. }
  169. /**
  170. * Sets the animation playback rate.
  171. *
  172. * @param {number} rate - The playback rate to set.
  173. */
  174. setPlaybackRate( rate ) {
  175. if ( rate !== 0 ) {
  176. this.mixer.timeScale = 1 / rate;
  177. } else {
  178. this.mixer.timeScale = 0;
  179. }
  180. }
  181. /**
  182. * Sets the wireframe material flag.
  183. *
  184. * @param {boolean} wireframeEnabled - Whether to enable wireframe rendering or not.
  185. */
  186. setWireframe( wireframeEnabled ) {
  187. if ( wireframeEnabled ) {
  188. if ( this.meshBody ) this.meshBody.material = this.meshBody.materialWireframe;
  189. if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialWireframe;
  190. } else {
  191. if ( this.meshBody ) this.meshBody.material = this.meshBody.materialTexture;
  192. if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialTexture;
  193. }
  194. }
  195. /**
  196. * Sets the skin defined by the given skin index. This will result in a different texture
  197. * for the body mesh.
  198. *
  199. * @param {number} index - The skin index.
  200. */
  201. setSkin( index ) {
  202. if ( this.meshBody && this.meshBody.material.wireframe === false ) {
  203. this.meshBody.material.map = this.skinsBody[ index ];
  204. }
  205. }
  206. /**
  207. * Sets the weapon defined by the given weapon index. This will result in a different weapon
  208. * hold by the character.
  209. *
  210. * @param {number} index - The weapon index.
  211. */
  212. setWeapon( index ) {
  213. for ( let i = 0; i < this.weapons.length; i ++ ) this.weapons[ i ].visible = false;
  214. const activeWeapon = this.weapons[ index ];
  215. if ( activeWeapon ) {
  216. activeWeapon.visible = true;
  217. this.meshWeapon = activeWeapon;
  218. this.syncWeaponAnimation();
  219. }
  220. }
  221. /**
  222. * Sets the defined animation clip as the active animation.
  223. *
  224. * @param {string} clipName - The name of the animation clip.
  225. */
  226. setAnimation( clipName ) {
  227. if ( this.meshBody ) {
  228. if ( this.meshBody.activeAction ) {
  229. this.meshBody.activeAction.stop();
  230. this.meshBody.activeAction = null;
  231. }
  232. const action = this.mixer.clipAction( clipName, this.meshBody );
  233. if ( action ) {
  234. this.meshBody.activeAction = action.play();
  235. }
  236. }
  237. this.activeClipName = clipName;
  238. this.syncWeaponAnimation();
  239. }
  240. /**
  241. * Synchronizes the weapon with the body animation.
  242. */
  243. syncWeaponAnimation() {
  244. const clipName = this.activeClipName;
  245. if ( this.meshWeapon ) {
  246. if ( this.meshWeapon.activeAction ) {
  247. this.meshWeapon.activeAction.stop();
  248. this.meshWeapon.activeAction = null;
  249. }
  250. const action = this.mixer.clipAction( clipName, this.meshWeapon );
  251. if ( action ) {
  252. this.meshWeapon.activeAction = action.syncWith( this.meshBody.activeAction ).play();
  253. }
  254. }
  255. }
  256. /**
  257. * Updates the animations of the mesh. Must be called inside the animation loop.
  258. *
  259. * @param {number} delta - The delta time in seconds.
  260. */
  261. update( delta ) {
  262. if ( this.mixer ) this.mixer.update( delta );
  263. }
  264. }
  265. export { MD2Character };
粤ICP备19079148号