spriteMap.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import { Engine } from "../Engines/engine.js";
  2. import { Vector2, Vector3 } from "../Maths/math.vector.js";
  3. import { Texture } from "../Materials/Textures/texture.js";
  4. import { RawTexture } from "../Materials/Textures/rawTexture.js";
  5. import { ShaderMaterial } from "../Materials/shaderMaterial.js";
  6. import { Effect } from "../Materials/effect.js";
  7. import { CreatePlane } from "../Meshes/Builders/planeBuilder.js";
  8. import "../Shaders/spriteMap.fragment.js";
  9. import "../Shaders/spriteMap.vertex.js";
  10. /**
  11. * Class used to manage a grid restricted sprite deployment on an Output plane.
  12. */
  13. export class SpriteMap {
  14. /** Returns the Number of Sprites in the System */
  15. get spriteCount() {
  16. return this.sprites.length;
  17. }
  18. /** Returns the Position of Output Plane*/
  19. get position() {
  20. return this._output.position;
  21. }
  22. /** Returns the Position of Output Plane*/
  23. set position(v) {
  24. this._output.position = v;
  25. }
  26. /** Returns the Rotation of Output Plane*/
  27. get rotation() {
  28. return this._output.rotation;
  29. }
  30. /** Returns the Rotation of Output Plane*/
  31. set rotation(v) {
  32. this._output.rotation = v;
  33. }
  34. /** Sets the AnimationMap*/
  35. get animationMap() {
  36. return this._animationMap;
  37. }
  38. /** Sets the AnimationMap*/
  39. set animationMap(v) {
  40. const buffer = v._texture._bufferView;
  41. const am = this._createTileAnimationBuffer(buffer);
  42. this._animationMap.dispose();
  43. this._animationMap = am;
  44. this._material.setTexture("animationMap", this._animationMap);
  45. }
  46. /**
  47. * Creates a new SpriteMap
  48. * @param name defines the SpriteMaps Name
  49. * @param atlasJSON is the JSON file that controls the Sprites Frames and Meta
  50. * @param spriteSheet is the Texture that the Sprites are on.
  51. * @param options a basic deployment configuration
  52. * @param scene The Scene that the map is deployed on
  53. */
  54. constructor(name, atlasJSON, spriteSheet, options, scene) {
  55. this.name = name;
  56. this.sprites = [];
  57. this.atlasJSON = atlasJSON;
  58. this.sprites = this.atlasJSON["frames"];
  59. this.spriteSheet = spriteSheet;
  60. /**
  61. * Run through the options and set what ever defaults are needed that where not declared.
  62. */
  63. this.options = options;
  64. options.stageSize = options.stageSize || new Vector2(1, 1);
  65. options.outputSize = options.outputSize || options.stageSize;
  66. options.outputPosition = options.outputPosition || Vector3.Zero();
  67. options.outputRotation = options.outputRotation || Vector3.Zero();
  68. options.layerCount = options.layerCount || 1;
  69. options.maxAnimationFrames = options.maxAnimationFrames || 0;
  70. options.baseTile = options.baseTile || 0;
  71. options.flipU = options.flipU || false;
  72. options.colorMultiply = options.colorMultiply || new Vector3(1, 1, 1);
  73. this._scene = scene;
  74. this._frameMap = this._createFrameBuffer();
  75. this._tileMaps = new Array();
  76. for (let i = 0; i < options.layerCount; i++) {
  77. this._tileMaps.push(this._createTileBuffer(null, i));
  78. }
  79. this._animationMap = this._createTileAnimationBuffer(null);
  80. const defines = [];
  81. defines.push("#define LAYERS " + options.layerCount);
  82. if (options.flipU) {
  83. defines.push("#define FLIPU");
  84. }
  85. defines.push(`#define MAX_ANIMATION_FRAMES ${options.maxAnimationFrames}.0`);
  86. const shaderString = Effect.ShadersStore["spriteMapPixelShader"];
  87. let layerSampleString;
  88. if (!scene.getEngine()._features.supportSwitchCaseInShader) {
  89. layerSampleString = "";
  90. for (let i = 0; i < options.layerCount; i++) {
  91. layerSampleString += `if (${i} == i) { frameID = texture2D(tileMaps[${i}], (tileID + 0.5) / stageSize, 0.).x; }`;
  92. }
  93. }
  94. else {
  95. layerSampleString = "switch(i) {";
  96. for (let i = 0; i < options.layerCount; i++) {
  97. layerSampleString += "case " + i + " : frameID = texture(tileMaps[" + i + "], (tileID + 0.5) / stageSize, 0.).x;";
  98. layerSampleString += "break;";
  99. }
  100. layerSampleString += "}";
  101. }
  102. Effect.ShadersStore["spriteMap" + this.name + "PixelShader"] = shaderString.replace("#define LAYER_ID_SWITCH", layerSampleString);
  103. this._material = new ShaderMaterial("spriteMap:" + this.name, this._scene, {
  104. vertex: "spriteMap",
  105. fragment: "spriteMap" + this.name,
  106. }, {
  107. defines,
  108. attributes: ["position", "normal", "uv"],
  109. uniforms: ["worldViewProjection", "time", "stageSize", "outputSize", "spriteMapSize", "spriteCount", "time", "colorMul", "mousePosition", "curTile", "flipU"],
  110. samplers: ["spriteSheet", "frameMap", "tileMaps", "animationMap"],
  111. needAlphaBlending: true,
  112. });
  113. this._time = 0;
  114. this._material.setFloat("spriteCount", this.spriteCount);
  115. this._material.setVector2("stageSize", options.stageSize);
  116. this._material.setVector2("outputSize", options.outputSize);
  117. this._material.setTexture("spriteSheet", this.spriteSheet);
  118. this._material.setVector2("spriteMapSize", new Vector2(1, 1));
  119. this._material.setVector3("colorMul", options.colorMultiply);
  120. let tickSave = 0;
  121. const bindSpriteTexture = () => {
  122. if (this.spriteSheet && this.spriteSheet.isReady()) {
  123. if (this.spriteSheet._texture) {
  124. this._material.setVector2("spriteMapSize", new Vector2(this.spriteSheet._texture.baseWidth || 1, this.spriteSheet._texture.baseHeight || 1));
  125. return;
  126. }
  127. }
  128. if (tickSave < 100) {
  129. setTimeout(() => {
  130. tickSave++;
  131. bindSpriteTexture();
  132. }, 100);
  133. }
  134. };
  135. bindSpriteTexture();
  136. this._material.setVector3("colorMul", options.colorMultiply);
  137. this._material.setTexture("frameMap", this._frameMap);
  138. this._material.setTextureArray("tileMaps", this._tileMaps);
  139. this._material.setTexture("animationMap", this._animationMap);
  140. this._material.setFloat("time", this._time);
  141. this._output = CreatePlane(name + ":output", { size: 1, updatable: true }, scene);
  142. this._output.scaling.x = options.outputSize.x;
  143. this._output.scaling.y = options.outputSize.y;
  144. this.position = options.outputPosition;
  145. this.rotation = options.outputRotation;
  146. const obfunction = () => {
  147. this._time += this._scene.getEngine().getDeltaTime();
  148. this._material.setFloat("time", this._time);
  149. };
  150. this._scene.onBeforeRenderObservable.add(obfunction);
  151. this._output.material = this._material;
  152. }
  153. /**
  154. * Returns tileID location
  155. * @returns Vector2 the cell position ID
  156. */
  157. getTileID() {
  158. const p = this.getMousePosition();
  159. p.multiplyInPlace(this.options.stageSize || Vector2.Zero());
  160. p.x = Math.floor(p.x);
  161. p.y = Math.floor(p.y);
  162. return p;
  163. }
  164. /**
  165. * Gets the UV location of the mouse over the SpriteMap.
  166. * @returns Vector2 the UV position of the mouse interaction
  167. */
  168. getMousePosition() {
  169. const out = this._output;
  170. const pickinfo = this._scene.pick(this._scene.pointerX, this._scene.pointerY, (mesh) => {
  171. if (mesh !== out) {
  172. return false;
  173. }
  174. return true;
  175. });
  176. if (!pickinfo || !pickinfo.hit || !pickinfo.getTextureCoordinates) {
  177. return new Vector2(-1, -1);
  178. }
  179. const coords = pickinfo.getTextureCoordinates();
  180. if (coords) {
  181. return coords;
  182. }
  183. return new Vector2(-1, -1);
  184. }
  185. /**
  186. * Creates the "frame" texture Buffer
  187. * -------------------------------------
  188. * Structure of frames
  189. * "filename": "Falling-Water-2.png",
  190. * "frame": {"x":69,"y":103,"w":24,"h":32},
  191. * "rotated": true,
  192. * "trimmed": true,
  193. * "spriteSourceSize": {"x":4,"y":0,"w":24,"h":32},
  194. * "sourceSize": {"w":32,"h":32}
  195. * @returns RawTexture of the frameMap
  196. */
  197. _createFrameBuffer() {
  198. const data = [];
  199. //Do two Passes
  200. for (let i = 0; i < this.spriteCount; i++) {
  201. data.push(0, 0, 0, 0); //frame
  202. data.push(0, 0, 0, 0); //spriteSourceSize
  203. data.push(0, 0, 0, 0); //sourceSize, rotated, trimmed
  204. data.push(0, 0, 0, 0); //Keep it pow2 cause I"m cool like that... it helps with sampling accuracy as well. Plus then we have 4 other parameters for future stuff.
  205. }
  206. //Second Pass
  207. for (let i = 0; i < this.spriteCount; i++) {
  208. const f = this.sprites[i]["frame"];
  209. const sss = this.sprites[i]["spriteSourceSize"];
  210. const ss = this.sprites[i]["sourceSize"];
  211. const r = this.sprites[i]["rotated"] ? 1 : 0;
  212. const t = this.sprites[i]["trimmed"] ? 1 : 0;
  213. //frame
  214. data[i * 4] = f.x;
  215. data[i * 4 + 1] = f.y;
  216. data[i * 4 + 2] = f.w;
  217. data[i * 4 + 3] = f.h;
  218. //spriteSourceSize
  219. data[i * 4 + this.spriteCount * 4] = sss.x;
  220. data[i * 4 + 1 + this.spriteCount * 4] = sss.y;
  221. data[i * 4 + 3 + this.spriteCount * 4] = sss.h;
  222. //sourceSize, rotated, trimmed
  223. data[i * 4 + this.spriteCount * 8] = ss.w;
  224. data[i * 4 + 1 + this.spriteCount * 8] = ss.h;
  225. data[i * 4 + 2 + this.spriteCount * 8] = r;
  226. data[i * 4 + 3 + this.spriteCount * 8] = t;
  227. }
  228. const floatArray = new Float32Array(data);
  229. const t = RawTexture.CreateRGBATexture(floatArray, this.spriteCount, 4, this._scene, false, false, Texture.NEAREST_NEAREST, Engine.TEXTURETYPE_FLOAT);
  230. return t;
  231. }
  232. /**
  233. * Creates the tileMap texture Buffer
  234. * @param buffer normally and array of numbers, or a false to generate from scratch
  235. * @param _layer indicates what layer for a logic trigger dealing with the baseTile. The system uses this
  236. * @returns RawTexture of the tileMap
  237. */
  238. _createTileBuffer(buffer, _layer = 0) {
  239. let data = [];
  240. const _ty = this.options.stageSize.y || 0;
  241. const _tx = this.options.stageSize.x || 0;
  242. if (!buffer) {
  243. let bt = this.options.baseTile;
  244. if (_layer != 0) {
  245. bt = 0;
  246. }
  247. for (let y = 0; y < _ty; y++) {
  248. for (let x = 0; x < _tx * 4; x += 4) {
  249. data.push(bt, 0, 0, 0);
  250. }
  251. }
  252. }
  253. else {
  254. data = buffer;
  255. }
  256. const floatArray = new Float32Array(data);
  257. const t = RawTexture.CreateRGBATexture(floatArray, _tx, _ty, this._scene, false, false, Texture.NEAREST_NEAREST, Engine.TEXTURETYPE_FLOAT);
  258. return t;
  259. }
  260. /**
  261. * Modifies the data of the tileMaps
  262. * @param _layer is the ID of the layer you want to edit on the SpriteMap
  263. * @param pos is the iVector2 Coordinates of the Tile
  264. * @param tile The SpriteIndex of the new Tile
  265. */
  266. changeTiles(_layer = 0, pos, tile = 0) {
  267. const buffer = this._tileMaps[_layer]._texture._bufferView;
  268. if (buffer === null) {
  269. return;
  270. }
  271. let p = [];
  272. if (pos instanceof Vector2) {
  273. p.push(pos);
  274. }
  275. else {
  276. p = pos;
  277. }
  278. const _tx = this.options.stageSize.x || 0;
  279. for (let i = 0; i < p.length; i++) {
  280. const _p = p[i];
  281. _p.x = Math.floor(_p.x);
  282. _p.y = Math.floor(_p.y);
  283. const id = _p.x * 4 + _p.y * (_tx * 4);
  284. buffer[id] = tile;
  285. }
  286. const t = this._createTileBuffer(buffer);
  287. this._tileMaps[_layer].dispose();
  288. this._tileMaps[_layer] = t;
  289. this._material.setTextureArray("tileMap", this._tileMaps);
  290. }
  291. /**
  292. * Creates the animationMap texture Buffer
  293. * @param buffer normally and array of numbers, or a false to generate from scratch
  294. * @returns RawTexture of the animationMap
  295. */
  296. _createTileAnimationBuffer(buffer) {
  297. const data = [];
  298. let floatArray;
  299. if (!buffer) {
  300. for (let i = 0; i < this.spriteCount; i++) {
  301. data.push(0, 0, 0, 0);
  302. let count = 1;
  303. while (count < (this.options.maxAnimationFrames || 4)) {
  304. data.push(0, 0, 0, 0);
  305. count++;
  306. }
  307. }
  308. floatArray = new Float32Array(data);
  309. }
  310. else {
  311. floatArray = buffer;
  312. }
  313. const t = RawTexture.CreateRGBATexture(floatArray, this.spriteCount, this.options.maxAnimationFrames || 4, this._scene, false, false, Texture.NEAREST_NEAREST, Engine.TEXTURETYPE_FLOAT);
  314. return t;
  315. }
  316. /**
  317. * Modifies the data of the animationMap
  318. * @param cellID is the Index of the Sprite
  319. * @param _frame is the target Animation frame
  320. * @param toCell is the Target Index of the next frame of the animation
  321. * @param time is a value between 0-1 that is the trigger for when the frame should change tiles
  322. * @param speed is a global scalar of the time variable on the map.
  323. */
  324. addAnimationToTile(cellID = 0, _frame = 0, toCell = 0, time = 0, speed = 1) {
  325. const buffer = this._animationMap._texture._bufferView;
  326. const id = cellID * 4 + this.spriteCount * 4 * _frame;
  327. if (!buffer) {
  328. return;
  329. }
  330. buffer[id] = toCell;
  331. buffer[id + 1] = time;
  332. buffer[id + 2] = speed;
  333. const t = this._createTileAnimationBuffer(buffer);
  334. this._animationMap.dispose();
  335. this._animationMap = t;
  336. this._material.setTexture("animationMap", this._animationMap);
  337. }
  338. /**
  339. * Exports the .tilemaps file
  340. */
  341. saveTileMaps() {
  342. let maps = "";
  343. for (let i = 0; i < this._tileMaps.length; i++) {
  344. if (i > 0) {
  345. maps += "\n\r";
  346. }
  347. maps += this._tileMaps[i]._texture._bufferView.toString();
  348. }
  349. const hiddenElement = document.createElement("a");
  350. hiddenElement.href = "data:octet/stream;charset=utf-8," + encodeURI(maps);
  351. hiddenElement.target = "_blank";
  352. hiddenElement.download = this.name + ".tilemaps";
  353. hiddenElement.click();
  354. hiddenElement.remove();
  355. }
  356. /**
  357. * Imports the .tilemaps file
  358. * @param url of the .tilemaps file
  359. */
  360. loadTileMaps(url) {
  361. const xhr = new XMLHttpRequest();
  362. xhr.open("GET", url);
  363. const _lc = this.options.layerCount || 0;
  364. xhr.onload = () => {
  365. const data = xhr.response.split("\n\r");
  366. for (let i = 0; i < _lc; i++) {
  367. const d = data[i].split(",").map(Number);
  368. const t = this._createTileBuffer(d);
  369. this._tileMaps[i].dispose();
  370. this._tileMaps[i] = t;
  371. }
  372. this._material.setTextureArray("tileMap", this._tileMaps);
  373. };
  374. xhr.send();
  375. }
  376. /**
  377. * Release associated resources
  378. */
  379. dispose() {
  380. this._output.dispose();
  381. this._material.dispose();
  382. this._animationMap.dispose();
  383. this._tileMaps.forEach((tm) => {
  384. tm.dispose();
  385. });
  386. this._frameMap.dispose();
  387. }
  388. }
  389. //# sourceMappingURL=spriteMap.js.map