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 */
32package jme3tools.optimize;
33
34import com.jme3.asset.AssetKey;
35import com.jme3.asset.AssetManager;
36import com.jme3.material.MatParamTexture;
37import com.jme3.material.Material;
38import com.jme3.math.Vector2f;
39import com.jme3.scene.Geometry;
40import com.jme3.scene.Mesh;
41import com.jme3.scene.Spatial;
42import com.jme3.scene.VertexBuffer;
43import com.jme3.scene.VertexBuffer.Type;
44import com.jme3.texture.Image;
45import com.jme3.texture.Image.Format;
46import com.jme3.texture.Texture;
47import com.jme3.texture.Texture2D;
48import com.jme3.util.BufferUtils;
49import java.lang.reflect.InvocationTargetException;
50import java.nio.ByteBuffer;
51import java.nio.FloatBuffer;
52import java.util.ArrayList;
53import java.util.HashMap;
54import java.util.List;
55import java.util.Map;
56import java.util.TreeMap;
57import java.util.logging.Level;
58import 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 */
119public 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
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     */
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     */
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     */
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     */
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
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
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
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
572        public boolean isLeaf() {
573            return child[0] == null && child[1] == null;
574        }
575
576        // Algorithm from http://www.blackpawn.com/texts/lightmaps/
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
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         */
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         */
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
670        public int getX() {
671            return x;
672        }
673
674        public int getY() {
675            return y;
676        }
677
678        public int getWidth() {
679            return width;
680        }
681
682        public int getHeight() {
683            return height;
684        }
685    }
686}
687