• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
34 import com.jme3.asset.AssetKey;
35 import com.jme3.asset.AssetManager;
36 import com.jme3.material.MatParamTexture;
37 import com.jme3.material.Material;
38 import com.jme3.math.Vector2f;
39 import com.jme3.scene.Geometry;
40 import com.jme3.scene.Mesh;
41 import com.jme3.scene.Spatial;
42 import com.jme3.scene.VertexBuffer;
43 import com.jme3.scene.VertexBuffer.Type;
44 import com.jme3.texture.Image;
45 import com.jme3.texture.Image.Format;
46 import com.jme3.texture.Texture;
47 import com.jme3.texture.Texture2D;
48 import com.jme3.util.BufferUtils;
49 import java.lang.reflect.InvocationTargetException;
50 import java.nio.ByteBuffer;
51 import java.nio.FloatBuffer;
52 import java.util.ArrayList;
53 import java.util.HashMap;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.TreeMap;
57 import java.util.logging.Level;
58 import java.util.logging.Logger;
59 
60 /**
61  * <b><code>TextureAtlas</code></b> allows combining multiple textures to one texture atlas.
62  *
63  * <p>After the TextureAtlas has been created with a certain size, textures can be added for
64  * freely chosen "map names". The textures are automatically placed on the atlas map and the
65  * image data is stored in a byte array for each map name. Later each map can be retrieved as
66  * a Texture to be used further in materials.</p>
67  *
68  * <p>The first map name used is the "master map" that defines new locations on the atlas. Secondary
69  * textures (other map names) have to reference a texture of the master map to position the texture
70  * on the secondary map. This is necessary as the maps share texture coordinates and thus need to be
71  * placed at the same location on both maps.</p>
72  *
73  * <p>The helper methods that work with <code>Geometry</code> objects handle the <em>DiffuseMap</em> or <em>ColorMap</em> as the master map and
74  * additionally handle <em>NormalMap</em> and <em>SpecularMap</em> as secondary maps.</p>
75  *
76  * <p>The textures are referenced by their <b>asset key name</b> and for each texture the location
77  * inside the atlas is stored. A texture with an existing key name is never added more than once
78  * to the atlas. You can access the information for each texture or geometry texture via helper methods.</p>
79  *
80  * <p>The TextureAtlas also allows you to change the texture coordinates of a mesh or geometry
81  * to point at the new locations of its texture inside the atlas (if the texture exists inside the atlas).</p>
82  *
83  * <p>Note that models that use texture coordinates outside the 0-1 range (repeating/wrapping textures)
84  * will not work correctly as their new coordinates leak into other parts of the atlas and thus display
85  * other textures instead of repeating the texture.</p>
86  *
87  * <p>Also note that textures are not scaled and the atlas needs to be large enough to hold all textures.
88  * All methods that allow adding textures return false if the texture could not be added due to the
89  * atlas being full. Furthermore secondary textures (normal, spcular maps etc.) have to be the same size
90  * as the main (e.g. DiffuseMap) texture.</p>
91  *
92  * <p><b>Usage examples</b></p>
93  * Create one geometry out of several geometries that are loaded from a j3o file:
94  * <pre>
95  * Node scene = assetManager.loadModel("Scenes/MyScene.j3o");
96  * Geometry geom = TextureAtlas.makeAtlasBatch(scene);
97  * rootNode.attachChild(geom);
98  * </pre>
99  * Create a texture atlas and change the texture coordinates of one geometry:
100  * <pre>
101  * Node scene = assetManager.loadModel("Scenes/MyScene.j3o");
102  * //either auto-create from node:
103  * TextureAtlas atlas = TextureAtlas.createAtlas(scene);
104  * //or create manually by adding textures or geometries with textures
105  * TextureAtlas atlas = new TextureAtlas(1024,1024);
106  * atlas.addTexture(myTexture, "DiffuseMap");
107  * atlas.addGeometry(myGeometry);
108  * //create material and set texture
109  * Material mat = new Material(mgr, "Common/MatDefs/Light/Lighting.j3md");
110  * mat.setTexture("DiffuseMap", atlas.getAtlasTexture("DiffuseMap"));
111  * //change one geometry to use atlas, apply texture coordinates and replace material.
112  * Geometry geom = scene.getChild("MyGeometry");
113  * atlas.applyCoords(geom);
114  * geom.setMaterial(mat);
115  * </pre>
116  *
117  * @author normenhansen, Lukasz Bruun - lukasz.dk
118  */
119 public class TextureAtlas {
120 
121     private static final Logger logger = Logger.getLogger(TextureAtlas.class.getName());
122     private Map<String, byte[]> images;
123     private int atlasWidth, atlasHeight;
124     private Format format = Format.ABGR8;
125     private Node root;
126     private Map<String, TextureAtlasTile> locationMap;
127     private Map<String, String> mapNameMap;
128     private String rootMapName;
129 
TextureAtlas(int width, int height)130     public TextureAtlas(int width, int height) {
131         this.atlasWidth = width;
132         this.atlasHeight = height;
133         root = new Node(0, 0, width, height);
134         locationMap = new TreeMap<String, TextureAtlasTile>();
135         mapNameMap = new HashMap<String, String>();
136     }
137 
138     /**
139      * Add a geometries DiffuseMap (or ColorMap), NormalMap and SpecularMap to the atlas.
140      * @param geometry
141      * @return false if the atlas is full.
142      */
addGeometry(Geometry geometry)143     public boolean addGeometry(Geometry geometry) {
144         Texture diffuse = getMaterialTexture(geometry, "DiffuseMap");
145         Texture normal = getMaterialTexture(geometry, "NormalMap");
146         Texture specular = getMaterialTexture(geometry, "SpecularMap");
147         if (diffuse == null) {
148             diffuse = getMaterialTexture(geometry, "ColorMap");
149 
150         }
151         if (diffuse != null && diffuse.getKey() != null) {
152             String keyName = diffuse.getKey().toString();
153             if (!addTexture(diffuse, "DiffuseMap")) {
154                 return false;
155             } else {
156                 if (normal != null && normal.getKey() != null) {
157                     addTexture(diffuse, "NormalMap", keyName);
158                 }
159                 if (specular != null && specular.getKey() != null) {
160                     addTexture(specular, "SpecularMap", keyName);
161                 }
162             }
163             return true;
164         }
165         return true;
166     }
167 
168     /**
169      * Add a texture for a specific map name
170      * @param texture A texture to add to the atlas.
171      * @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.
172      * @return false if the atlas is full.
173      */
addTexture(Texture texture, String mapName)174     public boolean addTexture(Texture texture, String mapName) {
175         if (texture == null) {
176             throw new IllegalStateException("Texture cannot be null!");
177         }
178         String name = textureName(texture);
179         if (texture.getImage() != null && name != null) {
180             return addImage(texture.getImage(), name, mapName, null);
181         } else {
182             throw new IllegalStateException("Texture has no asset key name!");
183         }
184     }
185 
186     /**
187      * Add a texture for a specific map name at the location of another existing texture on the master map.
188      * @param texture A texture to add to the atlas.
189      * @param mapName A freely chosen map name that can be later retrieved as a Texture.
190      * @param masterTexture The master texture for determining the location, it has to exist in tha master map.
191      */
addTexture(Texture texture, String mapName, Texture masterTexture)192     public void addTexture(Texture texture, String mapName, Texture masterTexture) {
193         String sourceTextureName = textureName(masterTexture);
194         if (sourceTextureName == null) {
195             throw new IllegalStateException("Supplied master map texture has no asset key name!");
196         } else {
197             addTexture(texture, mapName, sourceTextureName);
198         }
199     }
200 
201     /**
202      * Add a texture for a specific map name at the location of another existing texture (on the master map).
203      * @param texture A texture to add to the atlas.
204      * @param mapName A freely chosen map name that can be later retrieved as a Texture.
205      * @param sourceTextureName Name of the master map used for the location.
206      */
addTexture(Texture texture, String mapName, String sourceTextureName)207     public void addTexture(Texture texture, String mapName, String sourceTextureName) {
208         if (texture == null) {
209             throw new IllegalStateException("Texture cannot be null!");
210         }
211         String name = textureName(texture);
212         if (texture.getImage() != null && name != null) {
213             addImage(texture.getImage(), name, mapName, sourceTextureName);
214         } else {
215             throw new IllegalStateException("Texture has no asset key name!");
216         }
217     }
218 
textureName(Texture texture)219     private String textureName(Texture texture) {
220         if (texture == null) {
221             return null;
222         }
223         AssetKey key = texture.getKey();
224         if (key != null) {
225             return key.toString();
226         } else {
227             return null;
228         }
229     }
230 
addImage(Image image, String name, String mapName, String sourceTextureName)231     private boolean addImage(Image image, String name, String mapName, String sourceTextureName) {
232         if (rootMapName == null) {
233             rootMapName = mapName;
234         }
235         if (sourceTextureName == null && !rootMapName.equals(mapName)) {
236             throw new IllegalStateException("Atlas already has a master map called " + rootMapName + "."
237                     + " Textures for new maps have to use a texture from the master map for their location.");
238         }
239         TextureAtlasTile location = locationMap.get(name);
240         if (location != null) {
241             //have location for texture
242             if (!mapName.equals(mapNameMap.get(name))) {
243                 logger.log(Level.WARNING, "Same texture " + name + " is used in different maps! (" + mapName + " and " + mapNameMap.get(name) + "). Location will be based on location in " + mapNameMap.get(name) + "!");
244                 drawImage(image, location.getX(), location.getY(), mapName);
245                 return true;
246             } else {
247                 return true;
248             }
249         } else if (sourceTextureName == null) {
250             //need to make new tile
251             Node node = root.insert(image);
252             if (node == null) {
253                 return false;
254             }
255             location = node.location;
256         } else {
257             //got old tile to align to
258             location = locationMap.get(sourceTextureName);
259             if (location == null) {
260                 throw new IllegalStateException("Cannot find master map texture for " + name + ".");
261             } else if (location.width != image.getWidth() || location.height != image.getHeight()) {
262                 throw new IllegalStateException(mapName + " " + name + " does not fit " + rootMapName + " tile size. Make sure all textures (diffuse, normal, specular) for one model are the same size.");
263             }
264         }
265         mapNameMap.put(name, mapName);
266         locationMap.put(name, location);
267         drawImage(image, location.getX(), location.getY(), mapName);
268         return true;
269     }
270 
drawImage(Image source, int x, int y, String mapName)271     private void drawImage(Image source, int x, int y, String mapName) {
272         if (images == null) {
273             images = new HashMap<String, byte[]>();
274         }
275         byte[] image = images.get(mapName);
276         if (image == null) {
277             image = new byte[atlasWidth * atlasHeight * 4];
278             images.put(mapName, image);
279         }
280         //TODO: all buffers?
281         ByteBuffer sourceData = source.getData(0);
282         int height = source.getHeight();
283         int width = source.getWidth();
284         Image newImage = null;
285         for (int yPos = 0; yPos < height; yPos++) {
286             for (int xPos = 0; xPos < width; xPos++) {
287                 int i = ((xPos + x) + (yPos + y) * atlasWidth) * 4;
288                 if (source.getFormat() == Format.ABGR8) {
289                     int j = (xPos + yPos * width) * 4;
290                     image[i] = sourceData.get(j); //a
291                     image[i + 1] = sourceData.get(j + 1); //b
292                     image[i + 2] = sourceData.get(j + 2); //g
293                     image[i + 3] = sourceData.get(j + 3); //r
294                 } else if (source.getFormat() == Format.BGR8) {
295                     int j = (xPos + yPos * width) * 3;
296                     image[i] = 1; //a
297                     image[i + 1] = sourceData.get(j); //b
298                     image[i + 2] = sourceData.get(j + 1); //g
299                     image[i + 3] = sourceData.get(j + 2); //r
300                 } else if (source.getFormat() == Format.RGB8) {
301                     int j = (xPos + yPos * width) * 3;
302                     image[i] = 1; //a
303                     image[i + 1] = sourceData.get(j + 2); //b
304                     image[i + 2] = sourceData.get(j + 1); //g
305                     image[i + 3] = sourceData.get(j); //r
306                 } else if (source.getFormat() == Format.RGBA8) {
307                     int j = (xPos + yPos * width) * 4;
308                     image[i] = sourceData.get(j + 3); //a
309                     image[i + 1] = sourceData.get(j + 2); //b
310                     image[i + 2] = sourceData.get(j + 1); //g
311                     image[i + 3] = sourceData.get(j); //r
312                 } else if (source.getFormat() == Format.Luminance8) {
313                     int j = (xPos + yPos * width) * 1;
314                     image[i] = 1; //a
315                     image[i + 1] = sourceData.get(j); //b
316                     image[i + 2] = sourceData.get(j); //g
317                     image[i + 3] = sourceData.get(j); //r
318                 } else if (source.getFormat() == Format.Luminance8Alpha8) {
319                     int j = (xPos + yPos * width) * 2;
320                     image[i] = sourceData.get(j + 1); //a
321                     image[i + 1] = sourceData.get(j); //b
322                     image[i + 2] = sourceData.get(j); //g
323                     image[i + 3] = sourceData.get(j); //r
324                 } else {
325                     //ImageToAwt conversion
326                     if (newImage == null) {
327                         newImage = convertImageToAwt(source);
328                         if (newImage != null) {
329                             source = newImage;
330                             sourceData = source.getData(0);
331                             int j = (xPos + yPos * width) * 4;
332                             image[i] = sourceData.get(j); //a
333                             image[i + 1] = sourceData.get(j + 1); //b
334                             image[i + 2] = sourceData.get(j + 2); //g
335                             image[i + 3] = sourceData.get(j + 3); //r
336                         }else{
337                             throw new UnsupportedOperationException("Cannot draw or convert textures with format " + source.getFormat());
338                         }
339                     } else {
340                         throw new UnsupportedOperationException("Cannot draw textures with format " + source.getFormat());
341                     }
342                 }
343             }
344         }
345     }
346 
convertImageToAwt(Image source)347     private Image convertImageToAwt(Image source) {
348         //use awt dependent classes without actual dependency via reflection
349         try {
350             Class clazz = Class.forName("jme3tools.converters.ImageToAwt");
351             if (clazz == null) {
352                 return null;
353             }
354             Image newImage = new Image(format, source.getWidth(), source.getHeight(), BufferUtils.createByteBuffer(source.getWidth() * source.getHeight() * 4));
355             clazz.getMethod("convert", Image.class, Image.class).invoke(clazz.newInstance(), source, newImage);
356             return newImage;
357         } catch (InstantiationException ex) {
358         } catch (IllegalAccessException ex) {
359         } catch (IllegalArgumentException ex) {
360         } catch (InvocationTargetException ex) {
361         } catch (NoSuchMethodException ex) {
362         } catch (SecurityException ex) {
363         } catch (ClassNotFoundException ex) {
364         }
365         return null;
366     }
367 
368     /**
369      * Get the <code>TextureAtlasTile</code> for the given Texture
370      * @param texture The texture to retrieve the <code>TextureAtlasTile</code> for.
371      * @return
372      */
getAtlasTile(Texture texture)373     public TextureAtlasTile getAtlasTile(Texture texture) {
374         String sourceTextureName = textureName(texture);
375         if (sourceTextureName != null) {
376             return getAtlasTile(sourceTextureName);
377         }
378         return null;
379     }
380 
381     /**
382      * Get the <code>TextureAtlasTile</code> for the given Texture
383      * @param assetName The texture to retrieve the <code>TextureAtlasTile</code> for.
384      * @return
385      */
getAtlasTile(String assetName)386     private TextureAtlasTile getAtlasTile(String assetName) {
387         return locationMap.get(assetName);
388     }
389 
390     /**
391      * Creates a new atlas texture for the given map name.
392      * @param mapName
393      * @return
394      */
getAtlasTexture(String mapName)395     public Texture getAtlasTexture(String mapName) {
396         if (images == null) {
397             return null;
398         }
399         byte[] image = images.get(mapName);
400         if (image != null) {
401             Texture2D tex = new Texture2D(new Image(format, atlasWidth, atlasHeight, BufferUtils.createByteBuffer(image)));
402             tex.setMagFilter(Texture.MagFilter.Bilinear);
403             tex.setMinFilter(Texture.MinFilter.BilinearNearestMipMap);
404             tex.setWrap(Texture.WrapMode.Clamp);
405             return tex;
406         }
407         return null;
408     }
409 
410     /**
411      * Applies the texture coordinates to the given geometry
412      * if its DiffuseMap or ColorMap exists in the atlas.
413      * @param geom The geometry to change the texture coordinate buffer on.
414      * @return true if texture has been found and coords have been changed, false otherwise.
415      */
applyCoords(Geometry geom)416     public boolean applyCoords(Geometry geom) {
417         return applyCoords(geom, 0, geom.getMesh());
418     }
419 
420     /**
421      * Applies the texture coordinates to the given output mesh
422      * if the DiffuseMap or ColorMap of the input geometry exist in the atlas.
423      * @param geom The geometry to change the texture coordinate buffer on.
424      * @param offset Target buffer offset.
425      * @param outMesh The mesh to set the coords in (can be same as input).
426      * @return true if texture has been found and coords have been changed, false otherwise.
427      */
applyCoords(Geometry geom, int offset, Mesh outMesh)428     public boolean applyCoords(Geometry geom, int offset, Mesh outMesh) {
429         Mesh inMesh = geom.getMesh();
430         geom.computeWorldMatrix();
431 
432         VertexBuffer inBuf = inMesh.getBuffer(Type.TexCoord);
433         VertexBuffer outBuf = outMesh.getBuffer(Type.TexCoord);
434 
435         if (inBuf == null || outBuf == null) {
436             throw new IllegalStateException("Geometry mesh has no texture coordinate buffer.");
437         }
438 
439         Texture tex = getMaterialTexture(geom, "DiffuseMap");
440         if (tex == null) {
441             tex = getMaterialTexture(geom, "ColorMap");
442 
443         }
444         if (tex != null) {
445             TextureAtlasTile tile = getAtlasTile(tex);
446             if (tile != null) {
447                 FloatBuffer inPos = (FloatBuffer) inBuf.getData();
448                 FloatBuffer outPos = (FloatBuffer) outBuf.getData();
449                 tile.transformTextureCoords(inPos, offset, outPos);
450                 return true;
451             } else {
452                 return false;
453             }
454         } else {
455             throw new IllegalStateException("Geometry has no proper texture.");
456         }
457     }
458 
459     /**
460      * Create a texture atlas for the given root node, containing DiffuseMap, NormalMap and SpecularMap.
461      * @param root The rootNode to create the atlas for.
462      * @param atlasSize The size of the atlas (width and height).
463      * @return Null if the atlas cannot be created because not all textures fit.
464      */
createAtlas(Spatial root, int atlasSize)465     public static TextureAtlas createAtlas(Spatial root, int atlasSize) {
466         List<Geometry> geometries = new ArrayList<Geometry>();
467         GeometryBatchFactory.gatherGeoms(root, geometries);
468         TextureAtlas atlas = new TextureAtlas(atlasSize, atlasSize);
469         for (Geometry geometry : geometries) {
470             if (!atlas.addGeometry(geometry)) {
471                 logger.log(Level.WARNING, "Texture atlas size too small, cannot add all textures");
472                 return null;
473             }
474         }
475         return atlas;
476     }
477 
478     /**
479      * Creates one geometry out of the given root spatial and merges all single
480      * textures into one texture of the given size.
481      * @param spat The root spatial of the scene to batch
482      * @param mgr An assetmanager that can be used to create the material.
483      * @param atlasSize A size for the atlas texture, it has to be large enough to hold all single textures.
484      * @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.
485      */
makeAtlasBatch(Spatial spat, AssetManager mgr, int atlasSize)486     public static Geometry makeAtlasBatch(Spatial spat, AssetManager mgr, int atlasSize) {
487         List<Geometry> geometries = new ArrayList<Geometry>();
488         GeometryBatchFactory.gatherGeoms(spat, geometries);
489         TextureAtlas atlas = createAtlas(spat, atlasSize);
490         if (atlas == null) {
491             return null;
492         }
493         Geometry geom = new Geometry();
494         Mesh mesh = new Mesh();
495         GeometryBatchFactory.mergeGeometries(geometries, mesh);
496         applyAtlasCoords(geometries, mesh, atlas);
497         mesh.updateCounts();
498         mesh.updateBound();
499         geom.setMesh(mesh);
500 
501         Material mat = new Material(mgr, "Common/MatDefs/Light/Lighting.j3md");
502         mat.getAdditionalRenderState().setAlphaTest(true);
503         Texture diffuseMap = atlas.getAtlasTexture("DiffuseMap");
504         Texture normalMap = atlas.getAtlasTexture("NormalMap");
505         Texture specularMap = atlas.getAtlasTexture("SpecularMap");
506         if (diffuseMap != null) {
507             mat.setTexture("DiffuseMap", diffuseMap);
508         }
509         if (normalMap != null) {
510             mat.setTexture("NormalMap", normalMap);
511         }
512         if (specularMap != null) {
513             mat.setTexture("SpecularMap", specularMap);
514         }
515         mat.setFloat("Shininess", 16.0f);
516 
517         geom.setMaterial(mat);
518         return geom;
519     }
520 
applyAtlasCoords(List<Geometry> geometries, Mesh outMesh, TextureAtlas atlas)521     private static void applyAtlasCoords(List<Geometry> geometries, Mesh outMesh, TextureAtlas atlas) {
522         int globalVertIndex = 0;
523 
524         for (Geometry geom : geometries) {
525             Mesh inMesh = geom.getMesh();
526             geom.computeWorldMatrix();
527 
528             int geomVertCount = inMesh.getVertexCount();
529 
530             VertexBuffer inBuf = inMesh.getBuffer(Type.TexCoord);
531             VertexBuffer outBuf = outMesh.getBuffer(Type.TexCoord);
532 
533             if (inBuf == null || outBuf == null) {
534                 continue;
535             }
536 
537             atlas.applyCoords(geom, globalVertIndex, outMesh);
538 
539             globalVertIndex += geomVertCount;
540         }
541     }
542 
getMaterialTexture(Geometry geometry, String mapName)543     private static Texture getMaterialTexture(Geometry geometry, String mapName) {
544         Material mat = geometry.getMaterial();
545         if (mat == null || mat.getParam(mapName) == null || !(mat.getParam(mapName) instanceof MatParamTexture)) {
546             return null;
547         }
548         MatParamTexture param = (MatParamTexture) mat.getParam(mapName);
549         Texture texture = param.getTextureValue();
550         if (texture == null) {
551             return null;
552         }
553         return texture;
554 
555 
556     }
557 
558     private class Node {
559 
560         public TextureAtlasTile location;
561         public Node child[];
562         public boolean occupied;
563 
Node(int x, int y, int width, int height)564         public Node(int x, int y, int width, int height) {
565             location = new TextureAtlasTile(x, y, width, height);
566             child = new Node[2];
567             child[0] = null;
568             child[1] = null;
569             occupied = false;
570         }
571 
isLeaf()572         public boolean isLeaf() {
573             return child[0] == null && child[1] == null;
574         }
575 
576         // Algorithm from http://www.blackpawn.com/texts/lightmaps/
insert(Image image)577         public Node insert(Image image) {
578             if (!isLeaf()) {
579                 Node newNode = child[0].insert(image);
580 
581                 if (newNode != null) {
582                     return newNode;
583                 }
584 
585                 return child[1].insert(image);
586             } else {
587                 if (occupied) {
588                     return null; // occupied
589                 }
590 
591                 if (image.getWidth() > location.getWidth() || image.getHeight() > location.getHeight()) {
592                     return null; // does not fit
593                 }
594 
595                 if (image.getWidth() == location.getWidth() && image.getHeight() == location.getHeight()) {
596                     occupied = true; // perfect fit
597                     return this;
598                 }
599 
600                 int dw = location.getWidth() - image.getWidth();
601                 int dh = location.getHeight() - image.getHeight();
602 
603                 if (dw > dh) {
604                     child[0] = new Node(location.getX(), location.getY(), image.getWidth(), location.getHeight());
605                     child[1] = new Node(location.getX() + image.getWidth(), location.getY(), location.getWidth() - image.getWidth(), location.getHeight());
606                 } else {
607                     child[0] = new Node(location.getX(), location.getY(), location.getWidth(), image.getHeight());
608                     child[1] = new Node(location.getX(), location.getY() + image.getHeight(), location.getWidth(), location.getHeight() - image.getHeight());
609                 }
610 
611                 return child[0].insert(image);
612             }
613         }
614     }
615 
616     public class TextureAtlasTile {
617 
618         private int x;
619         private int y;
620         private int width;
621         private int height;
622 
TextureAtlasTile(int x, int y, int width, int height)623         public TextureAtlasTile(int x, int y, int width, int height) {
624             this.x = x;
625             this.y = y;
626             this.width = width;
627             this.height = height;
628         }
629 
630         /**
631          * Get the transformed texture coordinate for a given input location.
632          * @param previousLocation The old texture coordinate.
633          * @return The new texture coordinate inside the atlas.
634          */
getLocation(Vector2f previousLocation)635         public Vector2f getLocation(Vector2f previousLocation) {
636             float x = (float) getX() / (float) atlasWidth;
637             float y = (float) getY() / (float) atlasHeight;
638             float w = (float) getWidth() / (float) atlasWidth;
639             float h = (float) getHeight() / (float) atlasHeight;
640             Vector2f location = new Vector2f(x, y);
641             float prevX = previousLocation.x;
642             float prevY = previousLocation.y;
643             location.addLocal(prevX * w, prevY * h);
644             return location;
645         }
646 
647         /**
648          * Transforms a whole texture coordinates buffer.
649          * @param inBuf The input texture buffer.
650          * @param offset The offset in the output buffer
651          * @param outBuf The output buffer.
652          */
transformTextureCoords(FloatBuffer inBuf, int offset, FloatBuffer outBuf)653         public void transformTextureCoords(FloatBuffer inBuf, int offset, FloatBuffer outBuf) {
654             Vector2f tex = new Vector2f();
655 
656             // offset is given in element units
657             // convert to be in component units
658             offset *= 2;
659 
660             for (int i = 0; i < inBuf.capacity() / 2; i++) {
661                 tex.x = inBuf.get(i * 2 + 0);
662                 tex.y = inBuf.get(i * 2 + 1);
663                 Vector2f location = getLocation(tex);
664                 //TODO: add proper texture wrapping for atlases..
665                 outBuf.put(offset + i * 2 + 0, location.x);
666                 outBuf.put(offset + i * 2 + 1, location.y);
667             }
668         }
669 
getX()670         public int getX() {
671             return x;
672         }
673 
getY()674         public int getY() {
675             return y;
676         }
677 
getWidth()678         public int getWidth() {
679             return width;
680         }
681 
getHeight()682         public int getHeight() {
683             return height;
684         }
685     }
686 }
687