Home | History | Annotate | Download | only in optimize
      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 
    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