TextureAtlas.java 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. /*
  2. * Copyright (c) 2009-2012 jMonkeyEngine
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are
  7. * met:
  8. *
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. *
  12. * * Redistributions in binary form must reproduce the above copyright
  13. * notice, this list of conditions and the following disclaimer in the
  14. * documentation and/or other materials provided with the distribution.
  15. *
  16. * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
  17. * may be used to endorse or promote products derived from this software
  18. * without specific prior written permission.
  19. *
  20. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  22. * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  23. * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  24. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  25. * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  26. * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  27. * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  28. * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  29. * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  30. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. */
  32. package jme3tools.optimize;
  33. import com.jme3.asset.AssetKey;
  34. import com.jme3.asset.AssetManager;
  35. import com.jme3.material.MatParamTexture;
  36. import com.jme3.material.Material;
  37. import com.jme3.math.Vector2f;
  38. import com.jme3.scene.Geometry;
  39. import com.jme3.scene.Mesh;
  40. import com.jme3.scene.Spatial;
  41. import com.jme3.scene.VertexBuffer;
  42. import com.jme3.scene.VertexBuffer.Type;
  43. import com.jme3.texture.Image;
  44. import com.jme3.texture.Image.Format;
  45. import com.jme3.texture.Texture;
  46. import com.jme3.texture.Texture2D;
  47. import com.jme3.util.BufferUtils;
  48. import java.nio.ByteBuffer;
  49. import java.nio.FloatBuffer;
  50. import java.util.ArrayList;
  51. import java.util.HashMap;
  52. import java.util.List;
  53. import java.util.Map;
  54. import java.util.TreeMap;
  55. import java.util.logging.Level;
  56. import java.util.logging.Logger;
  57. /**
  58. *
  59. * @author normenhansen, Lukasz Bruun - lukasz.dk
  60. */
  61. public class TextureAtlas {
  62. private static final Logger logger = Logger.getLogger(TextureAtlas.class.getName());
  63. private Map<String, byte[]> images;
  64. private int atlasWidth, atlasHeight;
  65. private Format format = Format.ABGR8;
  66. private Node root;
  67. private Map<String, TextureAtlasTile> locationMap;
  68. private String rootMapName;
  69. public TextureAtlas(int width, int height) {
  70. this.atlasWidth = width;
  71. this.atlasHeight = height;
  72. root = new Node(0, 0, width, height);
  73. locationMap = new TreeMap<String, TextureAtlasTile>();
  74. }
  75. /**
  76. * Add a geometries DiffuseMap (or ColorMap), NormalMap and SpecularMap to the atlas.
  77. * @param geometry
  78. * @return false if the atlas is full.
  79. */
  80. public boolean addGeometry(Geometry geometry) {
  81. Texture diffuse = getMaterialTexture(geometry, "DiffuseMap");
  82. Texture normal = getMaterialTexture(geometry, "NormalMap");
  83. Texture specular = getMaterialTexture(geometry, "SpecularMap");
  84. if (diffuse == null) {
  85. diffuse = getMaterialTexture(geometry, "ColorMap");
  86. }
  87. if (diffuse != null && diffuse.getKey() != null) {
  88. String keyName = diffuse.getKey().getName();
  89. if (!addTexture(diffuse, "DiffuseMap")) {
  90. return false;
  91. } else {
  92. if (normal != null && normal.getKey() != null) {
  93. addTexture(diffuse, "NormalMap", keyName);
  94. }
  95. if (specular != null && specular.getKey() != null) {
  96. addTexture(specular, "SpecularMap", keyName);
  97. }
  98. }
  99. return true;
  100. }
  101. return true;
  102. }
  103. /**
  104. * Add a texture for a specific map name
  105. * @param texture A texture to add to the atlas.
  106. * @param mapName A freely chosen map name that can be later retrieved as a Texture. The first map name supplied will be the master map.
  107. * @return false if the atlas is full.
  108. */
  109. public boolean addTexture(Texture texture, String mapName) {
  110. if (texture == null) {
  111. throw new IllegalStateException("Texture cannot be null");
  112. }
  113. String name = textureName(texture);
  114. if (texture.getImage() != null && name != null) {
  115. return addImage(texture.getImage(), name, mapName, null);
  116. } else {
  117. throw new IllegalStateException("Texture has no asset name");
  118. }
  119. }
  120. /**
  121. * Add a texture for a specific map name at the location of another existing texture on the master map.
  122. * @param texture A texture to add to the atlas.
  123. * @param mapName A freely chosen map name that can be later retrieved as a Texture.
  124. * @param masterTexture The master texture for determining the location, it has to exist in tha master map.
  125. * @return false if the atlas is full
  126. */
  127. public void addTexture(Texture texture, String mapName, Texture masterTexture) {
  128. String sourceTextureName = textureName(masterTexture);
  129. if (sourceTextureName == null) {
  130. throw new IllegalStateException("Master texture has no asset name");
  131. } else {
  132. addTexture(texture, mapName, sourceTextureName);
  133. }
  134. }
  135. /**
  136. * Add a texture for a specific map name at the location of another existing texture (on the master map).
  137. * @param texture A texture to add to the atlas.
  138. * @param mapName A freely chosen map name that can be later retrieved as a Texture.
  139. * @param sourceTextureName Name of the master map used for the location.
  140. */
  141. public void addTexture(Texture texture, String mapName, String sourceTextureName) {
  142. if (texture == null) {
  143. throw new IllegalStateException("Texture cannot be null");
  144. }
  145. String name = textureName(texture);
  146. if (texture.getImage() != null && name != null) {
  147. addImage(texture.getImage(), name, mapName, sourceTextureName);
  148. } else {
  149. throw new IllegalStateException("Texture has no asset name");
  150. }
  151. }
  152. private String textureName(Texture texture) {
  153. if (texture == null) {
  154. return null;
  155. }
  156. AssetKey key = texture.getKey();
  157. if (key != null) {
  158. return key.getName();
  159. } else {
  160. return null;
  161. }
  162. }
  163. private boolean addImage(Image image, String name, String mapName, String sourceTextureName) {
  164. if (rootMapName == null) {
  165. rootMapName = mapName;
  166. }
  167. if (sourceTextureName == null && !rootMapName.equals(mapName)) {
  168. throw new IllegalStateException("Cannot add texture " + name + " to new map without source texture.");
  169. }
  170. TextureAtlasTile location = locationMap.get(name);
  171. if (location != null) {
  172. locationMap.put(name, location);
  173. return true;
  174. } else if (sourceTextureName == null) {
  175. Node node = root.insert(image);
  176. if (node == null) {
  177. return false;
  178. }
  179. location = node.location;
  180. } else {
  181. location = locationMap.get(sourceTextureName);
  182. if (location == null) {
  183. throw new IllegalStateException("Cannot find source texture for " + name + ".");
  184. } else if (location.width != image.getWidth() || location.height != image.getHeight()) {
  185. throw new IllegalStateException("Secondary texture " + name + " does not fit main texture size.");
  186. }
  187. }
  188. locationMap.put(name, location);
  189. drawImage(image, location.getX(), location.getY(), mapName);
  190. return true;
  191. }
  192. private void drawImage(Image source, int x, int y, String mapName) {
  193. if (images == null) {
  194. images = new HashMap<String, byte[]>();
  195. }
  196. byte[] image = images.get(mapName);
  197. if (image == null) {
  198. image = new byte[atlasWidth * atlasHeight * 4];
  199. images.put(mapName, image);
  200. }
  201. //TODO: all buffers?
  202. ByteBuffer sourceData = source.getData(0);
  203. int height = source.getHeight();
  204. int width = source.getWidth();
  205. for (int yPos = 0; yPos < height; yPos++) {
  206. for (int xPos = 0; xPos < width; xPos++) {
  207. int i = ((xPos + x) + (yPos + y) * atlasWidth) * 4;
  208. if (source.getFormat() == Format.ABGR8) {
  209. int j = (xPos + yPos * width) * 4;
  210. image[i] = sourceData.get(j); //a
  211. image[i + 1] = sourceData.get(j + 1); //b
  212. image[i + 2] = sourceData.get(j + 2); //g
  213. image[i + 3] = sourceData.get(j + 3); //r
  214. } else if (source.getFormat() == Format.BGR8) {
  215. int j = (xPos + yPos * width) * 3;
  216. image[i] = 1; //a
  217. image[i + 1] = sourceData.get(j); //b
  218. image[i + 2] = sourceData.get(j + 1); //g
  219. image[i + 3] = sourceData.get(j + 2); //r
  220. } else if (source.getFormat() == Format.RGB8) {
  221. int j = (xPos + yPos * width) * 3;
  222. image[i] = 1; //a
  223. image[i + 1] = sourceData.get(j + 2); //b
  224. image[i + 2] = sourceData.get(j + 1); //g
  225. image[i + 3] = sourceData.get(j); //r
  226. } else if (source.getFormat() == Format.RGBA8) {
  227. int j = (xPos + yPos * width) * 4;
  228. image[i] = sourceData.get(j + 3); //a
  229. image[i + 1] = sourceData.get(j + 2); //b
  230. image[i + 2] = sourceData.get(j + 1); //g
  231. image[i + 3] = sourceData.get(j); //r
  232. } else {
  233. throw new UnsupportedOperationException("Could not draw texture with format " + source.getFormat());
  234. }
  235. }
  236. }
  237. }
  238. /**
  239. * Get the <code>TextureAtlasTile</code> for the given Texture
  240. * @param texture The texture to retrieve the <code>TextureAtlasTile</code> for.
  241. * @return
  242. */
  243. public TextureAtlasTile getAtlasTile(Texture texture) {
  244. String sourceTextureName = textureName(texture);
  245. if (sourceTextureName != null) {
  246. return getAtlasTile(sourceTextureName);
  247. }
  248. return null;
  249. }
  250. /**
  251. * Get the <code>TextureAtlasTile</code> for the given Texture
  252. * @param assetName The texture to retrieve the <code>TextureAtlasTile</code> for.
  253. * @return
  254. */
  255. private TextureAtlasTile getAtlasTile(String assetName) {
  256. return locationMap.get(assetName);
  257. }
  258. /**
  259. * Creates a new atlas texture for the given map name.
  260. * @param mapName
  261. * @return
  262. */
  263. public Texture getAtlasTexture(String mapName) {
  264. if (images == null) {
  265. return null;
  266. }
  267. byte[] image = images.get(mapName);
  268. if (image != null) {
  269. Texture2D tex = new Texture2D(new Image(format, atlasWidth, atlasHeight, BufferUtils.createByteBuffer(image)));
  270. tex.setMagFilter(Texture.MagFilter.Bilinear);
  271. tex.setMinFilter(Texture.MinFilter.BilinearNearestMipMap);
  272. tex.setWrap(Texture.WrapMode.Clamp);
  273. return tex;
  274. }
  275. return null;
  276. }
  277. /**
  278. * Applies the texture coordinates to the given geometry
  279. * if its DiffuseMap or ColorMap exists in the atlas.
  280. * @param geom The geometry to change the texture coordinate buffer on.
  281. * @return true if texture has been found and coords have been changed, false otherwise.
  282. */
  283. public boolean applyCoords(Geometry geom) {
  284. return applyCoords(geom, 0, geom.getMesh());
  285. }
  286. /**
  287. * Applies the texture coordinates to the given output mesh
  288. * if the DiffuseMap or ColorMap of the input geometry exist in the atlas.
  289. * @param geom The geometry to change the texture coordinate buffer on.
  290. * @param offset Target buffer offset.
  291. * @param outMesh The mesh to set the coords in (can be same as input).
  292. * @return true if texture has been found and coords have been changed, false otherwise.
  293. */
  294. public boolean applyCoords(Geometry geom, int offset, Mesh outMesh) {
  295. Mesh inMesh = geom.getMesh();
  296. geom.computeWorldMatrix();
  297. VertexBuffer inBuf = inMesh.getBuffer(Type.TexCoord);
  298. VertexBuffer outBuf = outMesh.getBuffer(Type.TexCoord);
  299. if (inBuf == null || outBuf == null) {
  300. throw new IllegalStateException("Geometry mesh has no texture coordinate buffer.");
  301. }
  302. Texture tex = getMaterialTexture(geom, "DiffuseMap");
  303. if (tex == null) {
  304. tex = getMaterialTexture(geom, "ColorMap");
  305. }
  306. if (tex != null) {
  307. TextureAtlasTile tile = getAtlasTile(tex);
  308. if (tile != null) {
  309. FloatBuffer inPos = (FloatBuffer) inBuf.getData();
  310. FloatBuffer outPos = (FloatBuffer) outBuf.getData();
  311. tile.transformTextureCoords(inPos, offset, outPos);
  312. return true;
  313. } else {
  314. return false;
  315. }
  316. } else {
  317. throw new IllegalStateException("Geometry has no proper texture.");
  318. }
  319. }
  320. /**
  321. * Create a texture atlas for the given root node, containing DiffuseMap, NormalMap and SpecularMap.
  322. * @param root The rootNode to create the atlas for.
  323. * @param atlasSize The size of the atlas (width and height).
  324. * @return Null if the atlas cannot be created because not all textures fit.
  325. */
  326. public static TextureAtlas createAtlas(Spatial root, int atlasSize) {
  327. List<Geometry> geometries = new ArrayList<Geometry>();
  328. GeometryBatchFactory.gatherGeoms(root, geometries);
  329. TextureAtlas atlas = new TextureAtlas(atlasSize, atlasSize);
  330. for (Geometry geometry : geometries) {
  331. if(!atlas.addGeometry(geometry)){
  332. logger.log(Level.WARNING, "Texture atlas size too small, cannot add all textures");
  333. return null;
  334. }
  335. }
  336. return atlas;
  337. }
  338. /**
  339. * Creates one geometry out of the given root spatial and merges all single
  340. * textures into one texture of the given size.
  341. * @param spat The root spatial of the scene to batch
  342. * @param mgr An assetmanager that can be used to create the material.
  343. * @param atlasSize A size for the atlas texture, it has to be large enough to hold all single textures.
  344. * @return A new geometry that uses the generated texture atlas and merges all meshes of the root spatial, null if the atlas cannot be created because not all textures fit.
  345. */
  346. public static Geometry makeAtlasBatch(Spatial spat, AssetManager mgr, int atlasSize) {
  347. List<Geometry> geometries = new ArrayList<Geometry>();
  348. GeometryBatchFactory.gatherGeoms(spat, geometries);
  349. TextureAtlas atlas = createAtlas(spat, atlasSize);
  350. if (atlas == null) {
  351. return null;
  352. }
  353. Geometry geom = new Geometry();
  354. Mesh mesh = new Mesh();
  355. GeometryBatchFactory.mergeGeometries(geometries, mesh);
  356. applyAtlasCoords(geometries, mesh, atlas);
  357. mesh.updateCounts();
  358. mesh.updateBound();
  359. geom.setMesh(mesh);
  360. Material mat = new Material(mgr, "Common/MatDefs/Light/Lighting.j3md");
  361. mat.getAdditionalRenderState().setAlphaTest(true);
  362. Texture diffuseMap = atlas.getAtlasTexture("DiffuseMap");
  363. Texture normalMap = atlas.getAtlasTexture("NormalMap");
  364. Texture specularMap = atlas.getAtlasTexture("SpecularMap");
  365. if (diffuseMap != null) {
  366. mat.setTexture("DiffuseMap", diffuseMap);
  367. }
  368. if (normalMap != null) {
  369. mat.setTexture("NormalMap", normalMap);
  370. }
  371. if (specularMap != null) {
  372. mat.setTexture("SpecularMap", specularMap);
  373. }
  374. mat.setFloat("Shininess", 16.0f);
  375. geom.setMaterial(mat);
  376. return geom;
  377. }
  378. private static void applyAtlasCoords(List<Geometry> geometries, Mesh outMesh, TextureAtlas atlas) {
  379. int globalVertIndex = 0;
  380. for (Geometry geom : geometries) {
  381. Mesh inMesh = geom.getMesh();
  382. geom.computeWorldMatrix();
  383. int geomVertCount = inMesh.getVertexCount();
  384. VertexBuffer inBuf = inMesh.getBuffer(Type.TexCoord);
  385. VertexBuffer outBuf = outMesh.getBuffer(Type.TexCoord);
  386. if (inBuf == null || outBuf == null) {
  387. continue;
  388. }
  389. atlas.applyCoords(geom, globalVertIndex, outMesh);
  390. globalVertIndex += geomVertCount;
  391. }
  392. }
  393. private static Texture getMaterialTexture(Geometry geometry, String mapName) {
  394. Material mat = geometry.getMaterial();
  395. if (mat == null || mat.getParam(mapName) == null || !(mat.getParam(mapName) instanceof MatParamTexture)) {
  396. return null;
  397. }
  398. MatParamTexture param = (MatParamTexture) mat.getParam(mapName);
  399. Texture texture = param.getTextureValue();
  400. if (texture == null) {
  401. return null;
  402. }
  403. return texture;
  404. }
  405. private class Node {
  406. public TextureAtlasTile location;
  407. public Node child[];
  408. public boolean occupied;
  409. public Node(int x, int y, int width, int height) {
  410. location = new TextureAtlasTile(x, y, width, height);
  411. child = new Node[2];
  412. child[0] = null;
  413. child[1] = null;
  414. occupied = false;
  415. }
  416. public boolean isLeaf() {
  417. return child[0] == null && child[1] == null;
  418. }
  419. // Algorithm from http://www.blackpawn.com/texts/lightmaps/
  420. public Node insert(Image image) {
  421. if (!isLeaf()) {
  422. Node newNode = child[0].insert(image);
  423. if (newNode != null) {
  424. return newNode;
  425. }
  426. return child[1].insert(image);
  427. } else {
  428. if (occupied) {
  429. return null; // occupied
  430. }
  431. if (image.getWidth() > location.getWidth() || image.getHeight() > location.getHeight()) {
  432. return null; // does not fit
  433. }
  434. if (image.getWidth() == location.getWidth() && image.getHeight() == location.getHeight()) {
  435. occupied = true; // perfect fit
  436. return this;
  437. }
  438. int dw = location.getWidth() - image.getWidth();
  439. int dh = location.getHeight() - image.getHeight();
  440. if (dw > dh) {
  441. child[0] = new Node(location.getX(), location.getY(), image.getWidth(), location.getHeight());
  442. child[1] = new Node(location.getX() + image.getWidth(), location.getY(), location.getWidth() - image.getWidth(), location.getHeight());
  443. } else {
  444. child[0] = new Node(location.getX(), location.getY(), location.getWidth(), image.getHeight());
  445. child[1] = new Node(location.getX(), location.getY() + image.getHeight(), location.getWidth(), location.getHeight() - image.getHeight());
  446. }
  447. return child[0].insert(image);
  448. }
  449. }
  450. }
  451. public class TextureAtlasTile {
  452. private int x;
  453. private int y;
  454. private int width;
  455. private int height;
  456. public TextureAtlasTile(int x, int y, int width, int height) {
  457. this.x = x;
  458. this.y = y;
  459. this.width = width;
  460. this.height = height;
  461. }
  462. /**
  463. * Get the transformed texture location for a given input location.
  464. * @param previousLocation.
  465. * @return
  466. */
  467. public Vector2f getLocation(Vector2f previousLocation) {
  468. float x = (float) getX() / (float) atlasWidth;
  469. float y = (float) getY() / (float) atlasHeight;
  470. float w = (float) getWidth() / (float) atlasWidth;
  471. float h = (float) getHeight() / (float) atlasHeight;
  472. Vector2f location = new Vector2f(x, y);
  473. float prevX = previousLocation.x;
  474. float prevY = previousLocation.y;
  475. location.addLocal(prevX * w, prevY * h);
  476. return location;
  477. }
  478. /**
  479. * Transforms a whole texture coordinates buffer.
  480. * @param inBuf The input texture buffer.
  481. * @param offset The offset in the output buffer
  482. * @param outBuf The output buffer.
  483. */
  484. public void transformTextureCoords(FloatBuffer inBuf, int offset, FloatBuffer outBuf) {
  485. Vector2f tex = new Vector2f();
  486. // offset is given in element units
  487. // convert to be in component units
  488. offset *= 2;
  489. for (int i = 0; i < inBuf.capacity() / 2; i++) {
  490. tex.x = inBuf.get(i * 2 + 0);
  491. tex.y = inBuf.get(i * 2 + 1);
  492. Vector2f location = getLocation(tex);
  493. //TODO: add proper texture wrapping for atlases..
  494. outBuf.put(offset + i * 2 + 0, location.x);
  495. outBuf.put(offset + i * 2 + 1, location.y);
  496. }
  497. }
  498. public int getX() {
  499. return x;
  500. }
  501. public int getY() {
  502. return y;
  503. }
  504. public int getWidth() {
  505. return width;
  506. }
  507. public int getHeight() {
  508. return height;
  509. }
  510. }
  511. }