package org.newdawn.slick.opengl; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; import java.util.HashMap; import java.util.Iterator; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.ContextCapabilities; import org.lwjgl.opengl.EXTFramebufferObject; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL14; import org.lwjgl.opengl.GL30; import org.lwjgl.opengl.GLContext; import org.newdawn.slick.opengl.renderer.Renderer; import org.newdawn.slick.opengl.renderer.SGL; import org.newdawn.slick.util.ResourceLoader; /** * A texture loaded based on many old versions that will load image data from a file * and produce OpenGL textures. * * @see ImageData * * @author kevin */ public class InternalTextureLoader { /** Useful for debugging; keeps track of the current number of active textures. */ static int textureCount = 0; private static boolean forcePOT = true; public static boolean isPowerOfTwo(int n) { return (n & -n) == n; } /** * Returns true if we are forcing loaded image data into power-of-two OpenGL textures (by default, * this is true). If non-power-of-two textures is not supported in hardware (i.e. isNPOTSupported * returns false), then the image data will be forced into POT textures regardless of isForcePOTSize(). * * @return true if we should ensure POT sized textures, flase if we should attempt to use NPOT if supported */ public static boolean isForcePOT() { return forcePOT; } /** * Set whether we are forcing loaded image data into power-of-two OpenGL textures (by default, * this is true). If non-power-of-two textures is not supported in hardware (i.e. isNPOTSupported * returns false), then the image data will be forced into POT textures regardless of isForcePOTSize(). * * @param b true if we should ensure POT sized textures, flase if we should attempt to use NPOT if supported */ public static void setForcePOT(boolean b) { forcePOT = b; } /** * Returns the current number of active textures. Calling InternalTextureLoader.createTextureID * increases this number. Calling TextureImpl.release or InternalTextureLoader.deleteTextureID * decreases this number. * * @return the number of active OpenGL textures */ public static int getTextureCount() { return textureCount; } /** * Create a new texture ID; will increase the value for getTextureCount. * * @return A new texture ID */ public static int createTextureID() { IntBuffer tmp = createIntBuffer(1); GL.glGenTextures(tmp); textureCount++; return tmp.get(0); } /** * Used internally; call TextureImpl.release. * @param id the id of the OpenGL texture */ public static void deleteTextureID(int id) { IntBuffer texBuf = createIntBuffer(1); texBuf.put(id); texBuf.flip(); GL.glDeleteTextures(texBuf); textureCount--; } /** * Slick uses glGenerateMipmap() or GL14.GL_GENERATE_MIPMAP to automatically * build mipmaps (for advanced users). If neither of these versions are supported, * the GL_EXT_framebuffer_object is used as a fallback, and if that extension is also * missing, this method returns false. * * @return whether the version is >= 1.4 or GL_EXT_framebuffer_object extension exists */ public static boolean isGenerateMipmapSupported() { return GLContext.getCapabilities().OpenGL14 || GLContext.getCapabilities().GL_EXT_framebuffer_object; } /** * Returns true if non-power-of-two textures are supported in hardware via the * GL_ARB_texture_non_power_of_two extension. Non-power-of-two texture loading * is not a current feature of Slick, although it is planned. * * @return true if the extension is listed */ public static boolean isNPOTSupported() { //don't check GL20, nvidia/ATI usually don't advertise this extension //if it means requiring software fallback return GLContext.getCapabilities().GL_ARB_texture_non_power_of_two; } /** The renderer to use for all GL operations */ protected static SGL GL = Renderer.get(); /** The standard texture loaded used everywhere */ private static final InternalTextureLoader loader = new InternalTextureLoader(); /** * Get the single instance of this texture loader * * @return The single instance of the texture loader */ public static InternalTextureLoader get() { return loader; } /** The table of textures that have been loaded in this loader */ private HashMap texturesLinear = new HashMap(); /** The table of textures that have been loaded in this loader */ private HashMap texturesNearest = new HashMap(); /** The destination pixel format */ private int dstPixelFormat = SGL.GL_RGBA8; /** True if we're using deferred loading */ private boolean deferred; /** True if we should hold texture data */ private boolean holdTextureData; /** * Create a new texture loader based on the game panel */ private InternalTextureLoader() { } /** * Indicate where texture data should be held for reinitialising at a future * point. * * @param holdTextureData True if we should hold texture data */ public void setHoldTextureData(boolean holdTextureData) { this.holdTextureData = holdTextureData; } /** * True if we should only record the request to load in the intention * of loading the texture later * * @param deferred True if the we should load a token */ public void setDeferredLoading(boolean deferred) { this.deferred = deferred; } /** * Check if we're using deferred loading * * @return True if we're loading deferred textures */ public boolean isDeferredLoading() { return deferred; } /** * Remove a particular named image from the cache (does not release the OpenGL texture) * * @param name The name of the image to be cleared */ public void clear(String name) { texturesLinear.remove(name); texturesNearest.remove(name); } /** * Clear out the cached textures (does not release the OpenGL textures) */ public void clear() { texturesLinear.clear(); texturesNearest.clear(); } /** * Tell the loader to produce 16 bit textures */ public void set16BitMode() { dstPixelFormat = SGL.GL_RGBA16; } /** * Get a texture from a specific file * * @param source The file to load the texture from * @param flipped True if we should flip the texture on the y axis while loading * @param filter The filter to use * @return The texture loaded * @throws IOException Indicates a failure to load the image */ public Texture getTexture(File source, boolean flipped,int filter) throws IOException { String resourceName = source.getAbsolutePath(); InputStream in = new FileInputStream(source); return getTexture(in, resourceName, flipped, filter, null); } /** * Get a texture from a specific file * * @param source The file to load the texture from * @param flipped True if we should flip the texture on the y axis while loading * @param filter The filter to use * @param transparent The colour to interpret as transparent or null if none * @return The texture loaded * @throws IOException Indicates a failure to load the image */ public Texture getTexture(File source, boolean flipped,int filter, int[] transparent) throws IOException { String resourceName = source.getAbsolutePath(); InputStream in = new FileInputStream(source); return getTexture(in, resourceName, flipped, filter, transparent); } /** * Get a texture from a resource location * * @param resourceName The location to load the texture from * @param flipped True if we should flip the texture on the y axis while loading * @param filter The filter to use when scaling the texture * @return The texture loaded * @throws IOException Indicates a failure to load the image */ public Texture getTexture(String resourceName, boolean flipped, int filter) throws IOException { InputStream in = ResourceLoader.getResourceAsStream(resourceName); return getTexture(in, resourceName, flipped, filter, null); } /** * Get a texture from a resource location * * @param resourceName The location to load the texture from * @param flipped True if we should flip the texture on the y axis while loading * @param filter The filter to use when scaling the texture * @param transparent The colour to interpret as transparent or null if none * @return The texture loaded * @throws IOException Indicates a failure to load the image */ public Texture getTexture(String resourceName, boolean flipped, int filter, int[] transparent) throws IOException { InputStream in = ResourceLoader.getResourceAsStream(resourceName); return getTexture(in, resourceName, flipped, filter, transparent); } /** * Get a texture from a image file * * @param in The stream from which we can load the image * @param resourceName The name to give this image in the internal cache * @param flipped True if we should flip the image on the y-axis while loading * @param filter The filter to use when scaling the texture * @return The texture loaded * @throws IOException Indicates a failure to load the image */ public Texture getTexture(InputStream in, String resourceName, boolean flipped, int filter) throws IOException { return getTexture(in, resourceName, flipped, filter, null); } /** * Get a texture from a image file * * @param in The stream from which we can load the image * @param resourceName The name to give this image in the internal cache * @param flipped True if we should flip the image on the y-axis while loading * @param filter The filter to use when scaling the texture * @param transparent The colour to interpret as transparent or null if none * @return The texture loaded * @throws IOException Indicates a failure to load the image */ public TextureImpl getTexture(InputStream in, String resourceName, boolean flipped, int filter, int[] transparent) throws IOException { if (deferred) { return new DeferredTexture(in, resourceName, flipped, filter, transparent); } HashMap hash = texturesLinear; if (filter == SGL.GL_NEAREST) { hash = texturesNearest; } String resName = resourceName; if (transparent != null) { resName += ":"+transparent[0]+":"+transparent[1]+":"+transparent[2]; } resName += ":"+flipped; if (holdTextureData) { TextureImpl tex = (TextureImpl) hash.get(resName); if (tex != null) { return tex; } } else { SoftReference ref = (SoftReference) hash.get(resName); if (ref != null) { TextureImpl tex = (TextureImpl) ref.get(); if (tex != null) { return tex; } else { hash.remove(resName); } } } // horrible test until I can find something more suitable try { GL.glGetError(); } catch (NullPointerException e) { throw new RuntimeException("Image based resources must be loaded as part of init() or the game loop. They cannot be loaded before initialisation."); } TextureImpl tex = getTexture(in, resourceName, SGL.GL_TEXTURE_2D, filter, filter, flipped, transparent); tex.setCacheName(resName); if (holdTextureData) { hash.put(resName, tex); } else { hash.put(resName, new SoftReference(tex)); } return tex; } private TextureImpl getTexture(InputStream in, String resourceName, int target, int minFilter, int magFilter, boolean flipped, int[] transparent) throws IOException { // create the texture ID for this texture ByteBuffer textureBuffer; LoadableImageData imageData = ImageDataFactory.getImageDataFor(resourceName); textureBuffer = imageData.loadImage(new BufferedInputStream(in), flipped, transparent); int textureID = createTextureID(); TextureImpl texture = new TextureImpl(resourceName, target, textureID); // bind this texture GL.glEnable(target); GL.glBindTexture(target, textureID); int width; int height; int texWidth; int texHeight; ImageData.Format format; width = imageData.getWidth(); height = imageData.getHeight(); format = imageData.getFormat(); texture.setTextureWidth(imageData.getTexWidth()); texture.setTextureHeight(imageData.getTexHeight()); texWidth = texture.getTextureWidth(); texHeight = texture.getTextureHeight(); IntBuffer temp = BufferUtils.createIntBuffer(16); GL.glGetInteger(SGL.GL_MAX_TEXTURE_SIZE, temp); int max = temp.get(0); if ((texWidth > max) || (texHeight > max)) { throw new IOException("Attempt to allocate a texture to big for the current hardware"); } int srcPixelFormat = format.getOGLType(); int componentCount = format.getColorComponents(); texture.setWidth(width); texture.setHeight(height); texture.setImageFormat(format); if (holdTextureData) { texture.setTextureData(srcPixelFormat, componentCount, minFilter, magFilter, textureBuffer); } GL.glTexParameteri(target, SGL.GL_TEXTURE_MIN_FILTER, minFilter); GL.glTexParameteri(target, SGL.GL_TEXTURE_MAG_FILTER, magFilter); // produce a texture from the byte buffer GL.glTexImage2D(target, 0, dstPixelFormat, get2Fold(width), get2Fold(height), 0, srcPixelFormat, SGL.GL_UNSIGNED_BYTE, textureBuffer); return texture; } /** * An advanced texture loading method providing more parameters for glTexImage2D. * The created texture will not be placed in the cache. * * If genMipmaps is true, the loader will attempt to automatically build mipmaps * with either GL30.glGenerateMipmap() or GL14.GL_GENERATE_MIPMAP. If the GL version * is less than 1.4, then no mipmaps will be built and instead the magFilter (one * of GL_LINEAR or GL_NEAREST) will be used for both minification and magnification. * Users can determine mipmap generation support with isGenerateMipmapSupported(). * * If the internalFormat is not null, then that will override the default pixel * format described by this InternalTextureLoader (either GL_RGBA16 or GL_RGBA8 * depending on the is16BitMode() value). This parameter can be independent * of the format of ImageData -- OpenGL will convert the ImageData format (e.g. BGRA) * to the given internalFormat (e.g. RGB). Note that internalFormat is more limited * than the ImageData's format; i.e. BGRA as an internal storage format is only * supported if GL_ext_bgra is present. * * After calling this, the texture will be bound and the target (e.g. GL_TEXTURE_2D) * will be enabled. If you are using a higher priority target, such as 3D textures, * you should disable that afterwards to ensure compatibility with Slick. * * The ByteBuffer data is assumed to match getTexWidth/getTexHeight in ImageData. * * @param data the image data holding width, height, format (ImageData byte buffer is ignored) * @param buffer the actual data to send to GL * @param ref The name to give the TextureImpl * @param target The texture target we're loading this texture into * @param minFilter The scaling down filter * @param magFilter The scaling up filter * @param genMipmaps true to generate mipmaps (failure will fallback to using magFilter) * @param internalFormat the internal format of the texture (or null for default) * @return The texture loaded * @throws IOException Indicates a failure to load the image */ public TextureImpl createTexture(ImageData data, ByteBuffer buffer, String ref, int target, int minFilter, int magFilter, boolean genMipmaps, ImageData.Format internalFormat) throws IOException { int textureID = createTextureID(); TextureImpl texture = new TextureImpl(ref, target, textureID); // bind this texture GL.glEnable(target); GL.glBindTexture(target, textureID); int width = data.getWidth(); int height = data.getHeight(); int texWidth = data.getTexWidth(); int texHeight = data.getTexHeight(); boolean usePOT = !isNPOTSupported() || isForcePOT(); if (usePOT) { texWidth = get2Fold(width); texHeight = get2Fold(height); } int max = GL11.glGetInteger(SGL.GL_MAX_TEXTURE_SIZE); if (texWidth>max || texHeight>max) throw new IOException("Attempt to allocate a texture to big for the current hardware"); ImageData.Format dataFormat = data.getFormat(); int dstFmt = internalFormat!=null ? internalFormat.getOGLType() : dstPixelFormat; int srcFmt = dataFormat.getOGLType(); texture.setTextureWidth(texWidth); texture.setTextureHeight(texHeight); texture.setWidth(width); texture.setHeight(height); //even though it might really be RGBA16/8, user will expect comparability with Format constants texture.setImageFormat(internalFormat!=null ? internalFormat : ImageData.Format.RGBA); if (holdTextureData) { // TODO: fix the reload functionality; right now it causes problems and // should probably just be removed or reworked int componentCount = dataFormat.getColorComponents(); texture.setTextureData(srcFmt, componentCount, minFilter, magFilter, buffer); } ContextCapabilities cx = GLContext.getCapabilities(); if (genMipmaps && !isGenerateMipmapSupported()) { //nothing for auto mipmap gen minFilter = magFilter; genMipmaps = false; } GL.glTexParameteri(target, SGL.GL_TEXTURE_MIN_FILTER, minFilter); GL.glTexParameteri(target, SGL.GL_TEXTURE_MAG_FILTER, magFilter); //if we are < 3.0 and have no FBO support, fall back to GL_GENERATE_MIPMAP if (genMipmaps && !cx.OpenGL30 && !cx.GL_EXT_framebuffer_object) { GL.glTexParameteri(target, GL14.GL_GENERATE_MIPMAP, GL11.GL_TRUE); genMipmaps = false; } //For now, just assume Slick has decoded image data into POT GL.glTexImage2D(target, 0, dstFmt, texWidth, texHeight, 0, srcFmt, SGL.GL_UNSIGNED_BYTE, buffer); // if (texWidth==width && texHeight==height) { // GL.glTexImage2D(target, 0, dstFmt, texWidth, texHeight, // 0, srcFmt, SGL.GL_UNSIGNED_BYTE, buffer); // } else { // //Slick2D decodes NPOT image data into padded byte buffers. // //Once we make the shift to decoding NPOT image data, then we can clean this up // GL.glTexImage2D(target, 0, dstFmt, texWidth, texHeight, // 0, srcFmt, SGL.GL_UNSIGNED_BYTE, buffer); // // //first create the full texture // //we could also use a null ByteBuffer but this seems to be buggy with certain machines //// ByteBuffer empty = BufferUtils.createByteBuffer(texWidth * texHeight * 4); //// GL.glTexImage2D(target, 0, dstFmt, texWidth, texHeight, //// 0, SGL.GL_RGBA, SGL.GL_UNSIGNED_BYTE, empty); //// //then upload the sub image //// GL.glTexSubImage2D(target, 0, 0, 0, width, height, srcFmt, SGL.GL_UNSIGNED_BYTE, buffer); // } if (genMipmaps) { GL11.glEnable(target); //fixes ATI bug if (cx.OpenGL30) GL30.glGenerateMipmap(target); else EXTFramebufferObject.glGenerateMipmapEXT(target); } return texture; } /** * Create an empty texture * * @param width The width of the new texture * @param height The height of the new texture * @return The created empty texture * @throws IOException Indicates a failure to create the texture on the graphics hardware */ public Texture createTexture(final int width, final int height) throws IOException { return createTexture(width, height, SGL.GL_NEAREST); } /** * Create an empty texture * * @param width The width of the new texture * @param height The height of the new texture * @return The created empty texture * @throws IOException Indicates a failure to create the texture on the graphics hardware */ public Texture createTexture(final int width, final int height, final int filter) throws IOException { ImageData ds = new EmptyImageData(width, height); return getTexture(ds, filter); } /** * Get a texture from an image file. * * @param dataSource The image data to generate the texture from * @param filter The filter to use when scaling the texture * @return The texture created * @throws IOException Indicates the texture is too big for the hardware */ public Texture getTexture(ImageData dataSource, int filter) throws IOException { int target = SGL.GL_TEXTURE_2D; ByteBuffer textureBuffer; textureBuffer = dataSource.getImageBufferData(); // create the texture ID for this texture int textureID = createTextureID(); TextureImpl texture = new TextureImpl("generated:"+dataSource, target ,textureID); int minFilter = filter; int magFilter = filter; boolean flipped = false; // bind this texture GL.glEnable(target); GL.glBindTexture(target, textureID); int width; int height; int texWidth; int texHeight; ImageData.Format format; width = dataSource.getWidth(); height = dataSource.getHeight(); format = dataSource.getFormat(); texture.setTextureWidth(dataSource.getTexWidth()); texture.setTextureHeight(dataSource.getTexHeight()); texWidth = texture.getTextureWidth(); texHeight = texture.getTextureHeight(); int srcPixelFormat = format.getOGLType(); int componentCount = format.getColorComponents(); texture.setWidth(width); texture.setHeight(height); texture.setImageFormat(format); IntBuffer temp = BufferUtils.createIntBuffer(16); GL.glGetInteger(SGL.GL_MAX_TEXTURE_SIZE, temp); int max = temp.get(0); if ((texWidth > max) || (texHeight > max)) { throw new IOException("Attempt to allocate a texture to big for the current hardware"); } if (holdTextureData) { texture.setTextureData(srcPixelFormat, componentCount, minFilter, magFilter, textureBuffer); } GL.glTexParameteri(target, SGL.GL_TEXTURE_MIN_FILTER, minFilter); GL.glTexParameteri(target, SGL.GL_TEXTURE_MAG_FILTER, magFilter); // produce a texture from the byte buffer GL.glTexImage2D(target, 0, dstPixelFormat, get2Fold(width), get2Fold(height), 0, srcPixelFormat, SGL.GL_UNSIGNED_BYTE, textureBuffer); return texture; } /** * Get the closest greater power of 2 to the fold number * * @param fold The target number * @return The power of 2 */ public static int get2Fold(int fold) { //new algorithm? -> return 1 << (32 - Integer.numberOfLeadingZeros(n-1)); int ret = 2; while (ret < fold) { ret *= 2; } return ret; } /** * Creates an integer buffer to hold specified ints * - strictly a utility method * * @param size how many int to contain * @return created IntBuffer */ public static IntBuffer createIntBuffer(int size) { ByteBuffer temp = ByteBuffer.allocateDirect(4 * size); temp.order(ByteOrder.nativeOrder()); return temp.asIntBuffer(); } /** * Reload all the textures loaded in this loader */ public void reload() { Iterator texs = texturesLinear.values().iterator(); while (texs.hasNext()) { ((TextureImpl) texs.next()).reload(); } texs = texturesNearest.values().iterator(); while (texs.hasNext()) { ((TextureImpl) texs.next()).reload(); } } /** * Reload a given texture blob; used internally with setHoldTextureData. * Call TextureImpl.reload instead. * * @param texture The texture being reloaded * @param srcPixelFormat The source pixel format * @param componentCount The component count * @param minFilter The minification filter * @param magFilter The magnification filter * @param textureBuffer The pixel data * @return The ID of the newly created texture */ public int reload(TextureImpl texture, int srcPixelFormat, int componentCount, int minFilter, int magFilter, ByteBuffer textureBuffer) { int target = SGL.GL_TEXTURE_2D; int textureID = createTextureID(); GL.glEnable(target); GL.glBindTexture(target, textureID); GL.glTexParameteri(target, SGL.GL_TEXTURE_MIN_FILTER, minFilter); GL.glTexParameteri(target, SGL.GL_TEXTURE_MAG_FILTER, magFilter); // produce a texture from the byte buffer GL.glTexImage2D(target, 0, dstPixelFormat, texture.getTextureWidth(), texture.getTextureHeight(), 0, srcPixelFormat, SGL.GL_UNSIGNED_BYTE, textureBuffer); return textureID; } }