space.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
  2. import {game} from './game.js';
  3. import {graphics} from './graphics.js';
  4. import {math} from './math.js';
  5. import {visibility} from './visibility.js';
  6. import {particles} from './particles.js';
  7. import {blaster} from './blaster.js';
  8. import {OBJLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/OBJLoader.js';
  9. let _APP = null;
  10. const _NUM_BOIDS = 300;
  11. const _BOID_SPEED = 25;
  12. const _BOID_ACCELERATION = _BOID_SPEED / 2.5;
  13. const _BOID_FORCE_MAX = _BOID_ACCELERATION / 20.0;
  14. const _BOID_FORCE_ORIGIN = 50;
  15. const _BOID_FORCE_ALIGNMENT = 10;
  16. const _BOID_FORCE_SEPARATION = 20;
  17. const _BOID_FORCE_COLLISION = 50;
  18. const _BOID_FORCE_COHESION = 5;
  19. const _BOID_FORCE_WANDER = 3;
  20. class LineRenderer {
  21. constructor(game) {
  22. this._game = game;
  23. this._materials = {};
  24. this._group = new THREE.Group();
  25. this._game._graphics.Scene.add(this._group);
  26. }
  27. Reset() {
  28. this._lines = [];
  29. this._group.remove(...this._group.children);
  30. }
  31. Add(pt1, pt2, hexColour) {
  32. const geometry = new THREE.Geometry();
  33. geometry.vertices.push(pt1);
  34. geometry.vertices.push(pt2);
  35. let material = this._materials[hexColour];
  36. if (!material) {
  37. this._materials[hexColour] = new THREE.LineBasicMaterial(
  38. {color: hexColour});
  39. material = this._materials[hexColour];
  40. }
  41. const line = new THREE.Line(geometry, material);
  42. this._lines.push(line);
  43. this._group.add(line);
  44. }
  45. }
  46. class ExplodeParticles {
  47. constructor(game) {
  48. this._particleSystem = new particles.ParticleSystem(
  49. game, {texture: "./resources/blaster.jpg"});
  50. this._particles = [];
  51. }
  52. Splode(origin) {
  53. for (let i = 0; i < 128; i++) {
  54. const p = this._particleSystem.CreateParticle();
  55. p.Position.copy(origin);
  56. p.Velocity = new THREE.Vector3(
  57. math.rand_range(-1, 1),
  58. math.rand_range(-1, 1),
  59. math.rand_range(-1, 1)
  60. );
  61. p.Velocity.normalize();
  62. p.Velocity.multiplyScalar(125);
  63. p.TotalLife = 2.0;
  64. p.Life = p.TotalLife;
  65. p.Colours = [new THREE.Color(0xFF8000), new THREE.Color(0x800000)];
  66. p.Sizes = [3, 12];
  67. p.Size = p.Sizes[0];
  68. this._particles.push(p);
  69. }
  70. }
  71. Update(timeInSeconds) {
  72. this._particles = this._particles.filter(p => {
  73. return p.Alive;
  74. });
  75. for (const p of this._particles) {
  76. p.Life -= timeInSeconds;
  77. if (p.Life <= 0) {
  78. p.Alive = false;
  79. }
  80. p.Position.add(p.Velocity.clone().multiplyScalar(timeInSeconds));
  81. p.Velocity.multiplyScalar(0.75);
  82. p.Size = math.lerp(p.Life / p.TotalLife, p.Sizes[0], p.Sizes[1]);
  83. p.Colour.copy(p.Colours[0]);
  84. p.Colour.lerp(p.Colours[1], 1.0 - p.Life / p.TotalLife);
  85. }
  86. this._particleSystem.Update();
  87. }
  88. };
  89. class Boid {
  90. constructor(game, params) {
  91. this._mesh = new THREE.Mesh(
  92. params.geometry,
  93. new THREE.MeshStandardMaterial({color: 0x808080}));
  94. this._mesh.castShadow = true;
  95. this._mesh.receiveShadow = false;
  96. this._group = new THREE.Group();
  97. this._group.add(this._mesh);
  98. this._group.position.set(
  99. math.rand_range(-250, 250),
  100. math.rand_range(-250, 250),
  101. math.rand_range(-250, 250));
  102. this._direction = new THREE.Vector3(
  103. math.rand_range(-1, 1),
  104. math.rand_range(-1, 1),
  105. math.rand_range(-1, 1));
  106. this._velocity = this._direction.clone();
  107. const speedMultiplier = math.rand_range(params.speedMin, params.speedMax);
  108. this._maxSteeringForce = params.maxSteeringForce * speedMultiplier;
  109. this._maxSpeed = params.speed * speedMultiplier;
  110. this._acceleration = params.acceleration * speedMultiplier;
  111. const scale = 1.0 / speedMultiplier;
  112. this._radius = scale;
  113. this._mesh.scale.setScalar(scale * params.scale);
  114. //this._mesh.rotateX(Math.PI / 2);
  115. this._game = game;
  116. game._graphics.Scene.add(this._group);
  117. this._visibilityIndex = game._visibilityGrid.UpdateItem(
  118. this._mesh.uuid, this);
  119. this._wanderAngle = 0;
  120. this._seekGoal = params.seekGoal;
  121. this._fireCooldown = 0.0;
  122. this._params = params;
  123. }
  124. DisplayDebug() {
  125. const geometry = new THREE.SphereGeometry(10, 64, 64);
  126. const material = new THREE.MeshBasicMaterial({
  127. color: 0xFF0000,
  128. transparent: true,
  129. opacity: 0.25,
  130. });
  131. const mesh = new THREE.Mesh(geometry, material);
  132. this._group.add(mesh);
  133. this._mesh.material.color.setHex(0xFF0000);
  134. this._displayDebug = true;
  135. this._lineRenderer = new LineRenderer(this._game);
  136. }
  137. _UpdateDebug(local) {
  138. this._lineRenderer.Reset();
  139. this._lineRenderer.Add(
  140. this.Position, this.Position.clone().add(this._velocity),
  141. 0xFFFFFF);
  142. for (const e of local) {
  143. this._lineRenderer.Add(this.Position, e.Position, 0x00FF00);
  144. }
  145. }
  146. get Position() {
  147. return this._group.position;
  148. }
  149. get Velocity() {
  150. return this._velocity;
  151. }
  152. get Direction() {
  153. return this._direction;
  154. }
  155. get Radius() {
  156. return this._radius;
  157. }
  158. Step(timeInSeconds) {
  159. const local = this._game._visibilityGrid.GetLocalEntities(
  160. this.Position, 15);
  161. this._ApplySteering(timeInSeconds, local);
  162. const frameVelocity = this._velocity.clone();
  163. frameVelocity.multiplyScalar(timeInSeconds);
  164. this._group.position.add(frameVelocity);
  165. this._group.quaternion.setFromUnitVectors(
  166. new THREE.Vector3(0, 1, 0), this.Direction);
  167. this._visibilityIndex = this._game._visibilityGrid.UpdateItem(
  168. this._mesh.uuid, this, this._visibilityIndex);
  169. if (this._displayDebug) {
  170. this._UpdateDebug(local);
  171. }
  172. }
  173. _ApplySteering(timeInSeconds, local) {
  174. const separationVelocity = this._ApplySeparation(local);
  175. // Only apply alignment and cohesion to allies
  176. const allies = local.filter((e) => {
  177. return this._seekGoal.equals(e._seekGoal);
  178. });
  179. const enemies = local.filter((e) => {
  180. return !this._seekGoal.equals(e._seekGoal);
  181. });
  182. this._fireCooldown -= timeInSeconds;
  183. if (enemies.length > 0 && this._fireCooldown <= 0) {
  184. const p = this._game._blasters.CreateParticle();
  185. p.Start = this.Position.clone();
  186. p.End = this.Position.clone();
  187. p.Velocity = this.Direction.clone().multiplyScalar(300);
  188. p.Length = 50;
  189. p.Colours = [
  190. this._params.colour.clone(), new THREE.Color(0.0, 0.0, 0.0)];
  191. p.Life = 2.0;
  192. p.TotalLife = 2.0;
  193. p.Width = 0.25;
  194. if (Math.random() < 0.025) {
  195. this._game._explosionSystem.Splode(enemies[0].Position);
  196. }
  197. this._fireCooldown = 0.25;
  198. }
  199. const alignmentVelocity = this._ApplyAlignment(allies);
  200. const cohesionVelocity = this._ApplyCohesion(allies);
  201. const originVelocity = this._ApplySeek(this._seekGoal);
  202. const wanderVelocity = this._ApplyWander();
  203. const collisionVelocity = this._ApplyCollisionAvoidance();
  204. const steeringForce = new THREE.Vector3(0, 0, 0);
  205. steeringForce.add(separationVelocity);
  206. steeringForce.add(alignmentVelocity);
  207. steeringForce.add(cohesionVelocity);
  208. steeringForce.add(originVelocity);
  209. steeringForce.add(wanderVelocity);
  210. steeringForce.add(collisionVelocity);
  211. steeringForce.multiplyScalar(this._acceleration * timeInSeconds);
  212. // Clamp the force applied
  213. if (steeringForce.length() > this._maxSteeringForce) {
  214. steeringForce.normalize();
  215. steeringForce.multiplyScalar(this._maxSteeringForce);
  216. }
  217. this._velocity.add(steeringForce);
  218. // Clamp velocity
  219. if (this._velocity.length() > this._maxSpeed) {
  220. this._velocity.normalize();
  221. this._velocity.multiplyScalar(this._maxSpeed);
  222. }
  223. this._direction = this._velocity.clone();
  224. this._direction.normalize();
  225. }
  226. _ApplyCollisionAvoidance() {
  227. const colliders = this._game._visibilityGrid.GetGlobalItems();
  228. const ray = new THREE.Ray(this.Position, this.Direction);
  229. const force = new THREE.Vector3(0, 0, 0);
  230. for (const c of colliders) {
  231. if (c.Position.distanceTo(this.Position) > c.QuickRadius) {
  232. continue;
  233. }
  234. const result = ray.intersectBox(c.AABB, new THREE.Vector3());
  235. if (result) {
  236. const distanceToCollision = result.distanceTo(this.Position);
  237. if (distanceToCollision < 2) {
  238. let a = 0;
  239. }
  240. const dirToCenter = c.Position.clone().sub(this.Position).normalize();
  241. const dirToCollision = result.clone().sub(this.Position).normalize();
  242. const steeringDirection = dirToCollision.sub(dirToCenter).normalize();
  243. steeringDirection.multiplyScalar(_BOID_FORCE_COLLISION);
  244. force.add(steeringDirection);
  245. }
  246. }
  247. return force;
  248. }
  249. _ApplyWander() {
  250. this._wanderAngle += 0.1 * math.rand_range(-2 * Math.PI, 2 * Math.PI);
  251. const randomPointOnCircle = new THREE.Vector3(
  252. Math.cos(this._wanderAngle),
  253. 0,
  254. Math.sin(this._wanderAngle));
  255. const pointAhead = this._direction.clone();
  256. pointAhead.multiplyScalar(5);
  257. pointAhead.add(randomPointOnCircle);
  258. pointAhead.normalize();
  259. return pointAhead.multiplyScalar(_BOID_FORCE_WANDER);
  260. }
  261. _ApplySeparation(local) {
  262. if (local.length == 0) {
  263. return new THREE.Vector3(0, 0, 0);
  264. }
  265. const forceVector = new THREE.Vector3(0, 0, 0);
  266. for (let e of local) {
  267. const distanceToEntity = Math.max(
  268. e.Position.distanceTo(this.Position) - 1.5 * (this.Radius + e.Radius),
  269. 0.001);
  270. const directionFromEntity = new THREE.Vector3().subVectors(
  271. this.Position, e.Position);
  272. const multiplier = (_BOID_FORCE_SEPARATION / distanceToEntity);
  273. directionFromEntity.normalize();
  274. forceVector.add(
  275. directionFromEntity.multiplyScalar(multiplier));
  276. }
  277. return forceVector;
  278. }
  279. _ApplyAlignment(local) {
  280. const forceVector = new THREE.Vector3(0, 0, 0);
  281. for (let e of local) {
  282. const entityDirection = e.Direction;
  283. forceVector.add(entityDirection);
  284. }
  285. forceVector.normalize();
  286. forceVector.multiplyScalar(_BOID_FORCE_ALIGNMENT);
  287. return forceVector;
  288. }
  289. _ApplyCohesion(local) {
  290. const forceVector = new THREE.Vector3(0, 0, 0);
  291. if (local.length == 0) {
  292. return forceVector;
  293. }
  294. const averagePosition = new THREE.Vector3(0, 0, 0);
  295. for (let e of local) {
  296. averagePosition.add(e.Position);
  297. }
  298. averagePosition.multiplyScalar(1.0 / local.length);
  299. const directionToAveragePosition = averagePosition.clone().sub(
  300. this.Position);
  301. directionToAveragePosition.normalize();
  302. directionToAveragePosition.multiplyScalar(_BOID_FORCE_COHESION);
  303. // HACK: Floating point error from accumulation of positions.
  304. directionToAveragePosition.y = 0;
  305. return directionToAveragePosition;
  306. }
  307. _ApplySeek(destination) {
  308. const distance = Math.max(0,((
  309. this.Position.distanceTo(destination) - 50) / 500)) ** 2;
  310. const direction = destination.clone().sub(this.Position);
  311. direction.normalize();
  312. const forceVector = direction.multiplyScalar(
  313. _BOID_FORCE_ORIGIN * distance);
  314. return forceVector;
  315. }
  316. }
  317. class OpenWorldDemo extends game.Game {
  318. constructor() {
  319. super();
  320. }
  321. _OnInitialize() {
  322. this._entities = [];
  323. this._bloomPass = this._graphics.AddPostFX(
  324. graphics.PostFX.UnrealBloomPass,
  325. {
  326. threshold: 0.75,
  327. strength: 2.5,
  328. radius: 0,
  329. resolution: {
  330. x: 1024,
  331. y: 1024,
  332. }
  333. });
  334. this._glitchPass = this._graphics.AddPostFX(
  335. graphics.PostFX.GlitchPass, {});
  336. this._glitchCooldown = 15;
  337. this._glitchPass.enabled = false;
  338. this._LoadBackground();
  339. const geometries = {};
  340. const loader = new OBJLoader();
  341. loader.load("./resources/fighter.obj", (result) => {
  342. geometries.fighter = result.children[0].geometry;
  343. loader.load("./resources/cruiser.obj", (result) => {
  344. geometries.cruiser = result.children[0].geometry;
  345. this._CreateBoids(geometries);
  346. });
  347. });
  348. this._CreateEntities();
  349. }
  350. _LoadBackground() {
  351. const loader = new THREE.CubeTextureLoader();
  352. const texture = loader.load([
  353. './resources/space-posx.jpg',
  354. './resources/space-negx.jpg',
  355. './resources/space-posy.jpg',
  356. './resources/space-negy.jpg',
  357. './resources/space-posz.jpg',
  358. './resources/space-negz.jpg',
  359. ]);
  360. this._graphics._scene.background = texture;
  361. }
  362. _CreateEntities() {
  363. // This is 2D but eh, whatever.
  364. this._visibilityGrid = new visibility.VisibilityGrid(
  365. [new THREE.Vector3(-500, 0, -500), new THREE.Vector3(500, 0, 500)],
  366. [100, 100]);
  367. this._explosionSystem = new ExplodeParticles(this);
  368. this._blasters = new blaster.BlasterSystem(
  369. this, {texture: "./resources/blaster.jpg"});
  370. }
  371. _CreateBoids(geometries) {
  372. const positions = [
  373. new THREE.Vector3(-200, 50, -100),
  374. new THREE.Vector3(0, 0, 0)];
  375. const colours = [
  376. new THREE.Color(0.5, 0.5, 4.0),
  377. new THREE.Color(4.0, 0.5, 0.5)
  378. ];
  379. for (let i = 0; i < 2; i++) {
  380. const p = positions[i];
  381. const cruiser = new THREE.Mesh(
  382. geometries.cruiser,
  383. new THREE.MeshStandardMaterial({
  384. color: 0x404040
  385. }));
  386. cruiser.position.set(p.x, p.y, p.z);
  387. cruiser.castShadow = true;
  388. cruiser.receiveShadow = true;
  389. cruiser.rotation.x = Math.PI / 2;
  390. cruiser.scale.setScalar(10, 10, 10);
  391. cruiser.updateWorldMatrix();
  392. this._graphics.Scene.add(cruiser);
  393. cruiser.geometry.computeBoundingBox();
  394. const b = cruiser.geometry.boundingBox.clone().applyMatrix4(
  395. cruiser.matrixWorld);
  396. this._visibilityGrid.AddGlobalItem({
  397. Position: p,
  398. AABB: b,
  399. QuickRadius: 200,
  400. Velocity: new THREE.Vector3(0, 0, 0),
  401. Direction: new THREE.Vector3(0, 1, 0),
  402. });
  403. let params = {
  404. geometry: geometries.fighter,
  405. speedMin: 1.0,
  406. speedMax: 1.0,
  407. speed: _BOID_SPEED,
  408. maxSteeringForce: _BOID_FORCE_MAX,
  409. acceleration: _BOID_ACCELERATION,
  410. scale: 0.4,
  411. seekGoal: p,
  412. colour: colours[i]
  413. };
  414. for (let i = 0; i < _NUM_BOIDS; i++) {
  415. const e = new Boid(this, params);
  416. this._entities.push(e);
  417. }
  418. }
  419. //this._entities[0].DisplayDebug();
  420. }
  421. _OnStep(timeInSeconds) {
  422. timeInSeconds = Math.min(timeInSeconds, 1 / 10.0);
  423. this._blasters.Update(timeInSeconds);
  424. this._explosionSystem.Update(timeInSeconds);
  425. this._glitchCooldown -= timeInSeconds;
  426. if (this._glitchCooldown < 0) {
  427. this._glitchCooldown = math.rand_range(5, 10);
  428. this._glitchPass.enabled = !this._glitchPass.enabled;
  429. }
  430. this._StepEntities(timeInSeconds);
  431. }
  432. _StepEntities(timeInSeconds) {
  433. if (this._entities.length == 0) {
  434. return;
  435. }
  436. for (let e of this._entities) {
  437. e.Step(timeInSeconds);
  438. }
  439. }
  440. }
  441. function _Main() {
  442. _APP = new OpenWorldDemo();
  443. }
  444. _Main();