MD2Character.js 7.2 KB

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