Java tutorial
/***************************** BEGIN LICENSE BLOCK *************************** The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/MPL-1.1.html Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is the "Space Time Toolkit". The Initial Developer of the Original Code is the VAST team at the University of Alabama in Huntsville (UAH). <http://vast.uah.edu> Portions created by the Initial Developer are Copyright (C) 2007 the Initial Developer. All Rights Reserved. Please Contact Mike Botts <mike.botts@uah.edu> for more information. Contributor(s): Alexandre Robin <robin@nsstc.uah.edu> ******************************* END LICENSE BLOCK ***************************/ package org.vast.stt.renderer.opengl; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Enumeration; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; import javax.media.opengl.GL; import javax.media.opengl.glu.GLU; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.vast.ows.sld.Symbolizer; import org.vast.stt.style.DataStyler; import org.vast.stt.style.GridPatchGraphic; import org.vast.stt.style.RasterPixelGraphic; import org.vast.stt.style.RasterTileGraphic; import org.vast.stt.style.TextureStyler; import org.vast.util.MessageSystem; /** * <p><b>Title:</b><br/> * Texture Manager * </p> * * <p><b>Description:</b><br/> * Generate POT or NPOT textures according to OpenGL hardware * capabilities. POT textures are generated by padding with 100% * transparent white pixels, in which case texture coordinates * are automatically adjusted. * </p> * * <p>Copyright (c) 2007</p> * @author Alexandre Robin * @date Apr 13, 2006 * @version 1.0 */ public class TextureManager { protected Log log = LogFactory.getLog(TextureManager.class); protected static Hashtable<Symbolizer, GLTextureTable> symTextureTables = new Hashtable<Symbolizer, GLTextureTable>(); protected Hashtable<Symbolizer, Boolean> symTexturePoolSizeReachedTable = new Hashtable<Symbolizer, Boolean>(); protected Hashtable<Symbolizer, LinkedList<Integer>> symTextureStackTable = new Hashtable<Symbolizer, LinkedList<Integer>>(); protected GL gl; protected GLU glu; protected boolean forceNoExt = false; protected boolean npotSupported; protected boolean normalizationRequired; protected int maxSize = 512; protected int maxWastedPixels = 100; class GLTexture { protected int id = -1; protected boolean needsUpdate = true; protected int widthPadding; protected int heightPadding; protected List<GLTexture> tiles; } class GLTextureTable extends Hashtable<Object, GLTexture> { private final static long serialVersionUID = 0; } public TextureManager(GL gl, GLU glu) { this.gl = gl; this.glu = glu; // find out which texture 2D target to use String glExtensions = gl.glGetString(GL.GL_EXTENSIONS); if (!forceNoExt && glu.gluCheckExtension("GL_ARB_texture_rectangle", glExtensions) || glu.gluCheckExtension("GL_EXT_texture_rectangle", glExtensions)) { OpenGLCaps.TEXTURE_2D_TARGET = GL.GL_TEXTURE_RECTANGLE_EXT; log.info("NPOT textures supported"); npotSupported = true; normalizationRequired = false; } else { OpenGLCaps.TEXTURE_2D_TARGET = GL.GL_TEXTURE_2D; log.info("NPOT textures NOT supported. Textures will be padded with transparent pixels"); npotSupported = false; normalizationRequired = true; } // enable right texture target gl.glEnable(OpenGLCaps.TEXTURE_2D_TARGET); // Display max texture size int[] size = new int[1]; gl.glGetIntegerv(GL.GL_MAX_TEXTURE_SIZE, size, 0); log.info("Maximum texture size is " + size[0]); this.maxSize = size[0]; } /** * Retrieves stored textureID or create a new one along * with the corresponding texture in OpenGL memory. * @param styler * @param tex * @param force * @return */ public GLTexture getTexture(TextureStyler styler, RasterTileGraphic tex, boolean force) { GLTexture texInfo = null; Symbolizer sym = styler.getSymbolizer(); synchronized (symTextureTables) { // try to find table for this symbolizer GLTextureTable textureTable = symTextureTables.get(sym); // create if it doesn't exist if (textureTable == null) { textureTable = new GLTextureTable(); symTextureTables.put(sym, textureTable); } // try to find texture for this tile texInfo = textureTable.get(tex.block); // create if it doesn't exist if (texInfo == null) { texInfo = new GLTexture(); textureTable.put(tex.block, texInfo); } // create new texture if it needs update if (texInfo.needsUpdate || force) { texInfo.needsUpdate = false; createTexture(styler, tex, texInfo); if (log.isDebugEnabled()) log.debug("Tex #" + texInfo.id + " created for block " + tex.block); logStatistics(); } return texInfo; } } /** * Bind texture * @param tex * @return */ public void useTexture(GLTexture texInfo, RasterTileGraphic tex) { // otherwise just bind existing one if (texInfo.id > 0) { if (!gl.glIsTexture(texInfo.id)) log.debug("Tex #" + texInfo.id + " DOESN'T EXIST for block " + tex.block); else { gl.glBindTexture(OpenGLCaps.TEXTURE_2D_TARGET, texInfo.id); //log.debug("Tex #" + texInfo.id + " used"); } } // transfer padding info to RasterTileGraphic tex.heightPadding = texInfo.heightPadding; tex.widthPadding = texInfo.widthPadding; } /** * Creates a new texture by transfering data from styler to GL memory * @param styler * @param tex * @param texInfo */ protected void createTexture(TextureStyler styler, RasterTileGraphic tex, GLTexture texInfo) { // fetch texture data from styler fillTexData(styler, tex, texInfo); Symbolizer sym = styler.getSymbolizer(); // if texture was successfully constructed, bind it with GL if (tex.hasRasterData) { // size of texture pool int texPoolSize = styler.getSymbolizer().getTexPoolSize(); // create new texture name and bind it int[] id = new int[1]; boolean lastNewTexture = false; // ACCORDING TO WHETHER THE TEXTURE POOL SIZE HAS BEEN REACHED // A NEW TEXTURE IS GENERATED OR THE FIRST OF THE STACK IS REUSED // FOR THE MOST RECENT RASTER DATA if ((symTexturePoolSizeReachedTable == null) || !symTexturePoolSizeReachedTable.containsKey(sym)) { // create new texture name gl.glGenTextures(1, id, 0); if (texPoolSize > 0) { if (id[0] < (texPoolSize + 1)) { if (!symTextureStackTable.containsKey(sym)) { LinkedList<Integer> TexIdStack = new LinkedList<Integer>(); TexIdStack.addLast(id[0]); symTextureStackTable.put(sym, TexIdStack); } else { LinkedList<Integer> stack = symTextureStackTable.get(sym); stack.addLast(id[0]); symTextureStackTable.put(sym, stack); if (id[0] == texPoolSize) { symTexturePoolSizeReachedTable.put(sym, true); lastNewTexture = true; } } } } } else if ((symTexturePoolSizeReachedTable != null) || symTexturePoolSizeReachedTable.containsKey(sym)) { LinkedList<Integer> stack = symTextureStackTable.get(sym); id[0] = stack.poll(); stack.addLast(id[0]); } // Bind the texture to the texture Id gl.glBindTexture(OpenGLCaps.TEXTURE_2D_TARGET, id[0]); // set texture parameters // TODO: Allow user to select between Linear (smoothed) and nearest-neighbor interp // gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST); // gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST); gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR); gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR); gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE); gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE); //gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_BORDER); //gl.glTexParameteri(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_BORDER); //gl.glTexParameterfv(OpenGLCaps.TEXTURE_2D_TARGET, GL.GL_TEXTURE_BORDER_COLOR, new float[] {0.0f,0.0f,0.0f,0.0f}, 0); // figure out image format int format = 0; switch (tex.bands) { case 1: format = GL.GL_LUMINANCE; break; case 2: format = GL.GL_LUMINANCE_ALPHA; break; case 3: format = GL.GL_RGB; break; case 4: format = GL.GL_RGBA; break; } gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1); // create texture in GL memory if (!symTexturePoolSizeReachedTable.containsKey(sym) || lastNewTexture) { gl.glTexImage2D(OpenGLCaps.TEXTURE_2D_TARGET, 0, tex.bands, tex.width + texInfo.widthPadding, tex.height + texInfo.heightPadding, 0, format, GL.GL_UNSIGNED_BYTE, tex.rasterData); } else { gl.glTexImage2D(OpenGLCaps.TEXTURE_2D_TARGET, 0, tex.bands, tex.width + texInfo.widthPadding, tex.height + texInfo.heightPadding, 0, format, GL.GL_UNSIGNED_BYTE, tex.rasterData); // gl.glTexSubImage2D(OpenGLCaps.TEXTURE_2D_TARGET, 0, 0, 0, // tex.width + texInfo.widthPadding, tex.height + texInfo.heightPadding, // format, GL.GL_UNSIGNED_BYTE, tex.rasterData); } // erase temp buffer tex.rasterData = null; // set new id and reset needsUpdate flag int oldID = texInfo.id; texInfo.id = id[0]; // delete previous texture if needed if (oldID > 0) { gl.glDeleteTextures(1, new int[] { oldID }, 0); if (log.isDebugEnabled()) log.debug("Tex #" + oldID + " deleted and replaced by " + texInfo.id); } } } /** * Clears all textures used by this symbolizer * @param sym */ public void clearTextures(DataStyler styler) { synchronized (symTextureTables) { Symbolizer sym = styler.getSymbolizer(); GLTextureTable textureTable = symTextureTables.get(sym); if (textureTable != null) { Enumeration<GLTexture> textureEnum = textureTable.elements(); while (textureEnum.hasMoreElements()) { GLTexture texInfo = textureEnum.nextElement(); if (texInfo.id > 0) { gl.glDeleteTextures(1, new int[] { texInfo.id }, 0); if (log.isDebugEnabled()) log.debug("Tex #" + texInfo.id + " deleted for styler " + styler); } textureTable.remove(texInfo); } symTextureTables.remove(sym); } logStatistics(); } } /** * Clears texture used by this symbolizer and * associated with the given objects * @param sym * @param obj */ public void clearTextures(DataStyler styler, Object[] objects) { synchronized (symTextureTables) { Symbolizer sym = styler.getSymbolizer(); GLTextureTable textureTable = symTextureTables.get(sym); if (textureTable != null) { for (int i = 0; i < objects.length; i++) { GLTexture texInfo = textureTable.get(objects[i]); if (texInfo != null) { textureTable.remove(objects[i]); if (texInfo.id > 0) { gl.glDeleteTextures(1, new int[] { texInfo.id }, 0); if (log.isDebugEnabled()) log.debug("Tex #" + texInfo.id + " deleted for block " + objects[i]); } } else log.debug("Texture not found for block " + objects[i]); } } logStatistics(); } } /** * Create a texture based on data passed by styler * @param styler * @param tex * @param texInfo */ protected void fillTexData(TextureStyler styler, RasterTileGraphic tex, GLTexture texInfo) { int paddedWidth = tex.width; int paddedHeight = tex.height; int initialWidth = tex.width; int initialHeight = tex.height; // handle case of padding for npot if (!npotSupported) { // determine closest power of 2 paddedWidth = closestHigherPowerOfTwo(initialWidth); paddedHeight = closestHigherPowerOfTwo(initialHeight); // display warning message if padding is needed if (paddedWidth != initialWidth || paddedHeight != initialHeight) { MessageSystem.display("Texture will be padded to have a power of two size.\n" + " initial size: " + initialWidth + " x " + initialHeight + "\n" + " padded size: " + paddedWidth + " x " + paddedHeight, false); texInfo.widthPadding = paddedWidth - initialWidth; texInfo.heightPadding = paddedHeight - initialHeight; } if (log.isDebugEnabled()) log.debug( "Creating " + paddedWidth + " x " + paddedHeight + " Texture with " + tex.bands + " bands"); } // create byte buffer of the right size ByteBuffer buffer = ByteBuffer.allocateDirect(paddedWidth * paddedHeight * tex.bands); int index = 0; for (int j = 0; j < initialHeight; j++) { for (int i = 0; i < initialWidth; i++) { RasterPixelGraphic pixel = styler.getPixel(i + tex.xPos, j + tex.yPos); buffer.put(index, (byte) pixel.r); index++; // only if RGB if (tex.bands > 2) { buffer.put(index, (byte) pixel.g); index++; buffer.put(index, (byte) pixel.b); index++; } // only if RGBA if (tex.bands == 2 || tex.bands == 4) { buffer.put(index, (byte) pixel.a); index++; } } // skip padding bytes index += texInfo.widthPadding * tex.bands; } tex.rasterData = buffer; tex.hasRasterData = true; } /** * Split a Grid in several tiles with dimensions * equal to full powers of two * @param tex * @return */ protected List<GridPatchGraphic> splitGrid(GridPatchGraphic tex, List<RasterTileGraphic> rasterTiles) { return null; } /** * Split a Raster in several tiles with dimensions * equal to full powers of two * @param tex * @return */ public List<RasterTileGraphic> splitTexture(RasterTileGraphic tex) { List<Integer> widthList = getPower2SizeList(tex.width); List<Integer> heightList = getPower2SizeList(tex.height); int xSegs = widthList.size(); int ySegs = heightList.size(); List<RasterTileGraphic> tileList = new ArrayList<RasterTileGraphic>(xSegs * ySegs); int dX = 0; int dY = 0; for (int i = 0; i < xSegs; i++) { int width = widthList.get(i); int widthPadding = 0; if (i == xSegs - 1) widthPadding = width - (tex.width - dX); dY = 0; for (int j = 0; j < ySegs; j++) { RasterTileGraphic nextTile = new RasterTileGraphic(); int height = heightList.get(j); int heightPadding = 0; if (j == ySegs - 1) heightPadding = height - (tex.height - dY); nextTile.width = width; nextTile.height = height; nextTile.xPos = dX; nextTile.yPos = dY; nextTile.widthPadding = widthPadding; nextTile.heightPadding = heightPadding; dY += height; tileList.add(nextTile); } dX += width; } for (int i = 0; i < tileList.size(); i++) { RasterTileGraphic t = tileList.get(i); //GridPatchGraphic g = gridList.get(i); if (log.isDebugEnabled()) { log.debug("Tile: " + t.width + "x" + t.height + " @ " + t.xPos + "," + t.yPos + " pad " + t.widthPadding + "x" + t.heightPadding); } } return tileList; } /** * Breaks done the argument into a list of power of two values * @param size * @return */ protected List<Integer> getPower2SizeList(int size) { List<Integer> sizeList = new ArrayList<Integer>(); int remainSize = size; boolean done = false; do { int nextSize = closestHigherPowerOfTwo(remainSize); int wastedPixels = nextSize - remainSize; if (nextSize <= maxSize && wastedPixels <= maxWastedPixels) { done = true; } else { nextSize = closestLowerPowerOfTwo(remainSize); remainSize = remainSize - nextSize; } sizeList.add(nextSize); } while (!done); return sizeList; } /** * Calculate closest power of two value lower than argument * @param val * @return */ protected int closestLowerPowerOfTwo(int val) { int power = (int) Math.floor(log2(val)); int pow2 = (int) Math.pow(2, power); if (pow2 > maxSize) return maxSize; else return pow2; } /** * Calculate closest power of two value higher than argument * TODO closestHigherPowerOfTwo method description * @param val * @return */ protected int closestHigherPowerOfTwo(int val) { int power = (int) Math.ceil(log2(val)); return (int) Math.pow(2, power); } /** * Calculate log base 2 of the argument * @param val * @return */ protected double log2(double val) { return Math.log(val) / Math.log(2); } public boolean isNpotSupported() { return npotSupported; } public boolean isNormalizationRequired() { return normalizationRequired; } private void logStatistics() { if (log.isDebugEnabled()) { int texCount = 0; for (int i = 0; i < 65535; i++) if (gl.glIsTexture(i)) texCount++; log.debug("Num Tex = " + texCount); } } }