MD2CharacterComplex.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. import {
  2. Box3,
  3. MathUtils,
  4. MeshLambertMaterial,
  5. Object3D,
  6. TextureLoader,
  7. UVMapping,
  8. SRGBColorSpace
  9. } from 'three';
  10. import { MD2Loader } from '../loaders/MD2Loader.js';
  11. import { MorphBlendMesh } from '../misc/MorphBlendMesh.js';
  12. /**
  13. * This class represents a management component for animated MD2
  14. * character assets. It provides a larger API compared to {@link MD2Character}.
  15. */
  16. class MD2CharacterComplex {
  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 transition frames.
  37. *
  38. * @type {number}
  39. * @default 15
  40. */
  41. this.transitionFrames = 15;
  42. /**
  43. * The character's maximum speed.
  44. *
  45. * @type {number}
  46. * @default 275
  47. */
  48. this.maxSpeed = 275;
  49. /**
  50. * The character's maximum reverse speed.
  51. *
  52. * @type {number}
  53. * @default - 275
  54. */
  55. this.maxReverseSpeed = - 275;
  56. /**
  57. * The character's front acceleration.
  58. *
  59. * @type {number}
  60. * @default 600
  61. */
  62. this.frontAcceleration = 600;
  63. /**
  64. * The character's back acceleration.
  65. *
  66. * @type {number}
  67. * @default 600
  68. */
  69. this.backAcceleration = 600;
  70. /**
  71. * The character's front deceleration.
  72. *
  73. * @type {number}
  74. * @default 600
  75. */
  76. this.frontDeceleration = 600;
  77. /**
  78. * The character's angular speed.
  79. *
  80. * @type {number}
  81. * @default 2.5
  82. */
  83. this.angularSpeed = 2.5;
  84. /**
  85. * The root 3D object
  86. *
  87. * @type {Object3D}
  88. */
  89. this.root = new Object3D();
  90. /**
  91. * The body mesh.
  92. *
  93. * @type {?Mesh}
  94. * @default null
  95. */
  96. this.meshBody = null;
  97. /**
  98. * The weapon mesh.
  99. *
  100. * @type {?Mesh}
  101. * @default null
  102. */
  103. this.meshWeapon = null;
  104. /**
  105. * The movement controls.
  106. *
  107. * @type {Object}
  108. * @default null
  109. */
  110. this.controls = null;
  111. /**
  112. * The body skins.
  113. *
  114. * @type {Array<Texture>}
  115. */
  116. this.skinsBody = [];
  117. /**
  118. * The weapon skins.
  119. *
  120. * @type {Array<Texture>}
  121. */
  122. this.skinsWeapon = [];
  123. /**
  124. * The weapon meshes.
  125. *
  126. * @type {Array<Mesh>}
  127. */
  128. this.weapons = [];
  129. /**
  130. * The current skin.
  131. *
  132. * @type {Texture}
  133. * @default undefined
  134. */
  135. this.currentSkin = undefined;
  136. //
  137. this.onLoadComplete = function () {};
  138. // internals
  139. this.meshes = [];
  140. this.animations = {};
  141. this.loadCounter = 0;
  142. // internal movement control variables
  143. this.speed = 0;
  144. this.bodyOrientation = 0;
  145. this.walkSpeed = this.maxSpeed;
  146. this.crouchSpeed = this.maxSpeed * 0.5;
  147. // internal animation parameters
  148. this.activeAnimation = null;
  149. this.oldAnimation = null;
  150. // API
  151. }
  152. /**
  153. * Toggles shadow casting and receiving on the character's meshes.
  154. *
  155. * @param {boolean} enable - Whether to enable shadows or not.
  156. */
  157. enableShadows( enable ) {
  158. for ( let i = 0; i < this.meshes.length; i ++ ) {
  159. this.meshes[ i ].castShadow = enable;
  160. this.meshes[ i ].receiveShadow = enable;
  161. }
  162. }
  163. /**
  164. * Toggles visibility on the character's meshes.
  165. *
  166. * @param {boolean} enable - Whether the character is visible or not.
  167. */
  168. setVisible( enable ) {
  169. for ( let i = 0; i < this.meshes.length; i ++ ) {
  170. this.meshes[ i ].visible = enable;
  171. this.meshes[ i ].visible = enable;
  172. }
  173. }
  174. /**
  175. * Shares certain resources from a different character model.
  176. *
  177. * @param {MD2CharacterComplex} original - The original MD2 character.
  178. */
  179. shareParts( original ) {
  180. this.animations = original.animations;
  181. this.walkSpeed = original.walkSpeed;
  182. this.crouchSpeed = original.crouchSpeed;
  183. this.skinsBody = original.skinsBody;
  184. this.skinsWeapon = original.skinsWeapon;
  185. // BODY
  186. const mesh = this._createPart( original.meshBody.geometry, this.skinsBody[ 0 ] );
  187. mesh.scale.set( this.scale, this.scale, this.scale );
  188. this.root.position.y = original.root.position.y;
  189. this.root.add( mesh );
  190. this.meshBody = mesh;
  191. this.meshes.push( mesh );
  192. // WEAPONS
  193. for ( let i = 0; i < original.weapons.length; i ++ ) {
  194. const meshWeapon = this._createPart( original.weapons[ i ].geometry, this.skinsWeapon[ i ] );
  195. meshWeapon.scale.set( this.scale, this.scale, this.scale );
  196. meshWeapon.visible = false;
  197. meshWeapon.name = original.weapons[ i ].name;
  198. this.root.add( meshWeapon );
  199. this.weapons[ i ] = meshWeapon;
  200. this.meshWeapon = meshWeapon;
  201. this.meshes.push( meshWeapon );
  202. }
  203. }
  204. /**
  205. * Loads the character model for the given config.
  206. *
  207. * @param {Object} config - The config which defines the model and textures paths.
  208. */
  209. loadParts( config ) {
  210. const scope = this;
  211. function loadTextures( baseUrl, textureUrls ) {
  212. const textureLoader = new TextureLoader();
  213. const textures = [];
  214. for ( let i = 0; i < textureUrls.length; i ++ ) {
  215. textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
  216. textures[ i ].mapping = UVMapping;
  217. textures[ i ].name = textureUrls[ i ];
  218. textures[ i ].colorSpace = SRGBColorSpace;
  219. }
  220. return textures;
  221. }
  222. function checkLoadingComplete() {
  223. scope.loadCounter -= 1;
  224. if ( scope.loadCounter === 0 ) scope.onLoadComplete();
  225. }
  226. this.animations = config.animations;
  227. this.walkSpeed = config.walkSpeed;
  228. this.crouchSpeed = config.crouchSpeed;
  229. this.loadCounter = config.weapons.length * 2 + config.skins.length + 1;
  230. const weaponsTextures = [];
  231. for ( let i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
  232. // SKINS
  233. this.skinsBody = loadTextures( config.baseUrl + 'skins/', config.skins );
  234. this.skinsWeapon = loadTextures( config.baseUrl + 'skins/', weaponsTextures );
  235. // BODY
  236. const loader = new MD2Loader();
  237. loader.load( config.baseUrl + config.body, function ( geo ) {
  238. const boundingBox = new Box3();
  239. boundingBox.setFromBufferAttribute( geo.attributes.position );
  240. scope.root.position.y = - scope.scale * boundingBox.min.y;
  241. const mesh = scope._createPart( geo, scope.skinsBody[ 0 ] );
  242. mesh.scale.set( scope.scale, scope.scale, scope.scale );
  243. scope.root.add( mesh );
  244. scope.meshBody = mesh;
  245. scope.meshes.push( mesh );
  246. checkLoadingComplete();
  247. } );
  248. // WEAPONS
  249. const generateCallback = function ( index, name ) {
  250. return function ( geo ) {
  251. const mesh = scope._createPart( geo, scope.skinsWeapon[ index ] );
  252. mesh.scale.set( scope.scale, scope.scale, scope.scale );
  253. mesh.visible = false;
  254. mesh.name = name;
  255. scope.root.add( mesh );
  256. scope.weapons[ index ] = mesh;
  257. scope.meshWeapon = mesh;
  258. scope.meshes.push( mesh );
  259. checkLoadingComplete();
  260. };
  261. };
  262. for ( let i = 0; i < config.weapons.length; i ++ ) {
  263. loader.load( config.baseUrl + config.weapons[ i ][ 0 ], generateCallback( i, config.weapons[ i ][ 0 ] ) );
  264. }
  265. }
  266. /**
  267. * Sets the animation playback rate.
  268. *
  269. * @param {number} rate - The playback rate to set.
  270. */
  271. setPlaybackRate( rate ) {
  272. if ( this.meshBody ) this.meshBody.duration = this.meshBody.baseDuration / rate;
  273. if ( this.meshWeapon ) this.meshWeapon.duration = this.meshWeapon.baseDuration / rate;
  274. }
  275. /**
  276. * Sets the wireframe material flag.
  277. *
  278. * @param {boolean} wireframeEnabled - Whether to enable wireframe rendering or not.
  279. */
  280. setWireframe( wireframeEnabled ) {
  281. if ( wireframeEnabled ) {
  282. if ( this.meshBody ) this.meshBody.material = this.meshBody.materialWireframe;
  283. if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialWireframe;
  284. } else {
  285. if ( this.meshBody ) this.meshBody.material = this.meshBody.materialTexture;
  286. if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialTexture;
  287. }
  288. }
  289. /**
  290. * Sets the skin defined by the given skin index. This will result in a different texture
  291. * for the body mesh.
  292. *
  293. * @param {number} index - The skin index.
  294. */
  295. setSkin( index ) {
  296. if ( this.meshBody && this.meshBody.material.wireframe === false ) {
  297. this.meshBody.material.map = this.skinsBody[ index ];
  298. this.currentSkin = index;
  299. }
  300. }
  301. /**
  302. * Sets the weapon defined by the given weapon index. This will result in a different weapon
  303. * hold by the character.
  304. *
  305. * @param {number} index - The weapon index.
  306. */
  307. setWeapon( index ) {
  308. for ( let i = 0; i < this.weapons.length; i ++ ) this.weapons[ i ].visible = false;
  309. const activeWeapon = this.weapons[ index ];
  310. if ( activeWeapon ) {
  311. activeWeapon.visible = true;
  312. this.meshWeapon = activeWeapon;
  313. if ( this.activeAnimation ) {
  314. activeWeapon.playAnimation( this.activeAnimation );
  315. this.meshWeapon.setAnimationTime( this.activeAnimation, this.meshBody.getAnimationTime( this.activeAnimation ) );
  316. }
  317. }
  318. }
  319. /**
  320. * Sets the defined animation clip as the active animation.
  321. *
  322. * @param {string} animationName - The name of the animation clip.
  323. */
  324. setAnimation( animationName ) {
  325. if ( animationName === this.activeAnimation || ! animationName ) return;
  326. if ( this.meshBody ) {
  327. this.meshBody.setAnimationWeight( animationName, 0 );
  328. this.meshBody.playAnimation( animationName );
  329. this.oldAnimation = this.activeAnimation;
  330. this.activeAnimation = animationName;
  331. this.blendCounter = this.transitionFrames;
  332. }
  333. if ( this.meshWeapon ) {
  334. this.meshWeapon.setAnimationWeight( animationName, 0 );
  335. this.meshWeapon.playAnimation( animationName );
  336. }
  337. }
  338. update( delta ) {
  339. if ( this.controls ) this.updateMovementModel( delta );
  340. if ( this.animations ) {
  341. this.updateBehaviors();
  342. this.updateAnimations( delta );
  343. }
  344. }
  345. /**
  346. * Updates the animations of the mesh. Must be called inside the animation loop.
  347. *
  348. * @param {number} delta - The delta time in seconds.
  349. */
  350. updateAnimations( delta ) {
  351. let mix = 1;
  352. if ( this.blendCounter > 0 ) {
  353. mix = ( this.transitionFrames - this.blendCounter ) / this.transitionFrames;
  354. this.blendCounter -= 1;
  355. }
  356. if ( this.meshBody ) {
  357. this.meshBody.update( delta );
  358. this.meshBody.setAnimationWeight( this.activeAnimation, mix );
  359. this.meshBody.setAnimationWeight( this.oldAnimation, 1 - mix );
  360. }
  361. if ( this.meshWeapon ) {
  362. this.meshWeapon.update( delta );
  363. this.meshWeapon.setAnimationWeight( this.activeAnimation, mix );
  364. this.meshWeapon.setAnimationWeight( this.oldAnimation, 1 - mix );
  365. }
  366. }
  367. /**
  368. * Updates the animation state based on the control inputs.
  369. */
  370. updateBehaviors() {
  371. const controls = this.controls;
  372. const animations = this.animations;
  373. let moveAnimation, idleAnimation;
  374. // crouch vs stand
  375. if ( controls.crouch ) {
  376. moveAnimation = animations[ 'crouchMove' ];
  377. idleAnimation = animations[ 'crouchIdle' ];
  378. } else {
  379. moveAnimation = animations[ 'move' ];
  380. idleAnimation = animations[ 'idle' ];
  381. }
  382. // actions
  383. if ( controls.jump ) {
  384. moveAnimation = animations[ 'jump' ];
  385. idleAnimation = animations[ 'jump' ];
  386. }
  387. if ( controls.attack ) {
  388. if ( controls.crouch ) {
  389. moveAnimation = animations[ 'crouchAttack' ];
  390. idleAnimation = animations[ 'crouchAttack' ];
  391. } else {
  392. moveAnimation = animations[ 'attack' ];
  393. idleAnimation = animations[ 'attack' ];
  394. }
  395. }
  396. // set animations
  397. if ( controls.moveForward || controls.moveBackward || controls.moveLeft || controls.moveRight ) {
  398. if ( this.activeAnimation !== moveAnimation ) {
  399. this.setAnimation( moveAnimation );
  400. }
  401. }
  402. if ( Math.abs( this.speed ) < 0.2 * this.maxSpeed && ! ( controls.moveLeft || controls.moveRight || controls.moveForward || controls.moveBackward ) ) {
  403. if ( this.activeAnimation !== idleAnimation ) {
  404. this.setAnimation( idleAnimation );
  405. }
  406. }
  407. // set animation direction
  408. if ( controls.moveForward ) {
  409. if ( this.meshBody ) {
  410. this.meshBody.setAnimationDirectionForward( this.activeAnimation );
  411. this.meshBody.setAnimationDirectionForward( this.oldAnimation );
  412. }
  413. if ( this.meshWeapon ) {
  414. this.meshWeapon.setAnimationDirectionForward( this.activeAnimation );
  415. this.meshWeapon.setAnimationDirectionForward( this.oldAnimation );
  416. }
  417. }
  418. if ( controls.moveBackward ) {
  419. if ( this.meshBody ) {
  420. this.meshBody.setAnimationDirectionBackward( this.activeAnimation );
  421. this.meshBody.setAnimationDirectionBackward( this.oldAnimation );
  422. }
  423. if ( this.meshWeapon ) {
  424. this.meshWeapon.setAnimationDirectionBackward( this.activeAnimation );
  425. this.meshWeapon.setAnimationDirectionBackward( this.oldAnimation );
  426. }
  427. }
  428. }
  429. /**
  430. * Transforms the character model based on the control input.
  431. *
  432. * @param {number} delta - The delta time in seconds.
  433. */
  434. updateMovementModel( delta ) {
  435. function exponentialEaseOut( k ) {
  436. return k === 1 ? 1 : - Math.pow( 2, - 10 * k ) + 1;
  437. }
  438. const controls = this.controls;
  439. // speed based on controls
  440. if ( controls.crouch ) this.maxSpeed = this.crouchSpeed;
  441. else this.maxSpeed = this.walkSpeed;
  442. this.maxReverseSpeed = - this.maxSpeed;
  443. if ( controls.moveForward ) this.speed = MathUtils.clamp( this.speed + delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
  444. if ( controls.moveBackward ) this.speed = MathUtils.clamp( this.speed - delta * this.backAcceleration, this.maxReverseSpeed, this.maxSpeed );
  445. // orientation based on controls
  446. // (don't just stand while turning)
  447. const dir = 1;
  448. if ( controls.moveLeft ) {
  449. this.bodyOrientation += delta * this.angularSpeed;
  450. this.speed = MathUtils.clamp( this.speed + dir * delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
  451. }
  452. if ( controls.moveRight ) {
  453. this.bodyOrientation -= delta * this.angularSpeed;
  454. this.speed = MathUtils.clamp( this.speed + dir * delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
  455. }
  456. // speed decay
  457. if ( ! ( controls.moveForward || controls.moveBackward ) ) {
  458. if ( this.speed > 0 ) {
  459. const k = exponentialEaseOut( this.speed / this.maxSpeed );
  460. this.speed = MathUtils.clamp( this.speed - k * delta * this.frontDeceleration, 0, this.maxSpeed );
  461. } else {
  462. const k = exponentialEaseOut( this.speed / this.maxReverseSpeed );
  463. this.speed = MathUtils.clamp( this.speed + k * delta * this.backAcceleration, this.maxReverseSpeed, 0 );
  464. }
  465. }
  466. // displacement
  467. const forwardDelta = this.speed * delta;
  468. this.root.position.x += Math.sin( this.bodyOrientation ) * forwardDelta;
  469. this.root.position.z += Math.cos( this.bodyOrientation ) * forwardDelta;
  470. // steering
  471. this.root.rotation.y = this.bodyOrientation;
  472. }
  473. // internal
  474. _createPart( geometry, skinMap ) {
  475. const materialWireframe = new MeshLambertMaterial( { color: 0xffaa00, wireframe: true } );
  476. const materialTexture = new MeshLambertMaterial( { color: 0xffffff, wireframe: false, map: skinMap } );
  477. //
  478. const mesh = new MorphBlendMesh( geometry, materialTexture );
  479. mesh.rotation.y = - Math.PI / 2;
  480. //
  481. mesh.materialTexture = materialTexture;
  482. mesh.materialWireframe = materialWireframe;
  483. //
  484. mesh.autoCreateAnimations( this.animationFPS );
  485. return mesh;
  486. }
  487. }
  488. export { MD2CharacterComplex };
粤ICP备19079148号