diff --git a/pom.xml b/pom.xml index 6abd8367..f39bbfa7 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 net.opengrabeso glg2d - 0.6.3 + 0.7.1-SNAPSHOT jar GLG2D OpenGL Text Renderer for JOGL and LWJGL @@ -67,12 +67,6 @@ jogl-all-main ${jogl.version} - - net.opengrabeso - text-renderer - 0.1.14-SNAPSHOT - - junit junit diff --git a/src/main/java/net/opengrabeso/opengl/util/GLPixelAttributes.java b/src/main/java/net/opengrabeso/opengl/util/GLPixelAttributes.java new file mode 100644 index 00000000..b4676566 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/GLPixelAttributes.java @@ -0,0 +1,58 @@ +package net.opengrabeso.opengl.util; + +/** + * Pixel attributes. + */ +public class GLPixelAttributes { + /** + * Undefined instance of {@link GLPixelAttributes}, having componentCount:=0, format:=0 and type:= 0. + */ + public static final GLPixelAttributes UNDEF = new GLPixelAttributes(0, 0); + + /** + * The OpenGL pixel data format + */ + public final int format; + /** + * The OpenGL pixel data type + */ + public final int type; + + @Override + public final int hashCode() { + // 31 * x == (x << 5) - x + int hash = format; + return ((hash << 5) - hash) + type; + } + + @Override + public final boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof GLPixelAttributes) { + final GLPixelAttributes other = (GLPixelAttributes) obj; + return format == other.format && + type == other.type; + } else { + return false; + } + } + + /** + * Create a new {@link GLPixelAttributes} instance based on GL format and type. + * + * @param dataFormat GL data format + * @param dataType GL data type + + */ + public GLPixelAttributes(final int dataFormat, final int dataType) { + this.format = dataFormat; + this.type = dataType; + } + + @Override + public String toString() { + return "PixelAttributes[fmt 0x" + Integer.toHexString(format) + ", type 0x" + Integer.toHexString(type) + ", " + "]"; + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/awt/TextRenderer.java b/src/main/java/net/opengrabeso/opengl/util/awt/TextRenderer.java new file mode 100644 index 00000000..09746c78 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/awt/TextRenderer.java @@ -0,0 +1,1687 @@ +/* + * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. + * Copyright (c) 2010 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + * + * Sun gratefully acknowledges that this software was originally authored + * and developed by Kenneth Bradley Russell and Christopher John Kline. + */ +package net.opengrabeso.opengl.util.awt; + +import com.github.opengrabeso.jaagl.GL2; +import com.jogamp.common.nio.Buffers; +import net.opengrabeso.opengl.util.packrect.*; +import net.opengrabeso.opengl.util.texture.*; + +import java.awt.AlphaComposite; +import java.awt.Color; + +import java.awt.Font; +import java.awt.Frame; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.font.*; +import java.awt.geom.*; +import java.nio.*; +import java.text.*; +import java.util.*; + + +/** Renders bitmapped Java 2D text into an OpenGL window with high + performance, full Unicode support, and a simple API. Performs + appropriate caching of text rendering results in an OpenGL texture + internally to avoid repeated font rasterization. The caching is + completely automatic, does not require any user intervention, and + has no visible controls in the public API.

+ + Using the {@link TextRenderer TextRenderer} is simple. Add a + "TextRenderer renderer;" field to your {@link + com.jogamp.opengl.GLEventListener GLEventListener}. In your {@link + com.jogamp.opengl.GLEventListener#init init} method, add: + +

+    renderer = new TextRenderer(new Font("SansSerif", Font.BOLD, 36));
+    
+ +

In the {@link com.jogamp.opengl.GLEventListener#display display} method of your + {@link com.jogamp.opengl.GLEventListener GLEventListener}, add: +

+    renderer.beginRendering(drawable.getWidth(), drawable.getHeight());
+    // optionally set the color
+    renderer.setColor(1.0f, 0.2f, 0.2f, 0.8f);
+    renderer.draw("Text to draw", xPosition, yPosition);
+    // ... more draw commands, color changes, etc.
+    renderer.endRendering();
+    
+ + Unless you are sharing textures and display lists between OpenGL + contexts, you do not need to call the {@link #dispose dispose} + method of the TextRenderer; the OpenGL resources it uses + internally will be cleaned up automatically when the OpenGL + context is destroyed.

+ + Note that the TextRenderer may cause the vertex and texture + coordinate array buffer bindings to change, or to be unbound. This + is important to note if you are using Vertex Buffer Objects (VBOs) + in your application.

+ + Internally, the renderer uses a rectangle packing algorithm to + pack both glyphs and full Strings' rendering results (which are + variable size) onto a larger OpenGL texture. The internal backing + store is maintained using a {@link + com.jogamp.opengl.util.awt.TextureRenderer TextureRenderer}. A least + recently used (LRU) algorithm is used to discard previously + rendered strings; the specific algorithm is undefined, but is + currently implemented by flushing unused Strings' rendering + results every few hundred rendering cycles, where a rendering + cycle is defined as a pair of calls to {@link #beginRendering + beginRendering} / {@link #endRendering endRendering}. + + @author John Burkey + @author Kenneth Russell +*/ +public class TextRenderer { + + // These are occasionally useful for more in-depth debugging + private static final boolean DISABLE_GLYPH_CACHE = false; + + static final int kSize = 256; + + // Every certain number of render cycles, flush the strings which + // haven't been used recently + private static final int CYCLES_PER_FLUSH = 100; + + // The amount of vertical dead space on the backing store before we + // force a compaction + private static final float MAX_VERTICAL_FRAGMENTATION = 0.7f; + static final int kQuadsPerBuffer = 100; + static final int kCoordsPerVertVerts = 3; + static final int kCoordsPerVertTex = 2; + static final int kVertsPerQuad = 4; + static final int kTotalBufferSizeVerts = kQuadsPerBuffer * kVertsPerQuad; + static final int kTotalBufferSizeCoordsVerts = kQuadsPerBuffer * kVertsPerQuad * kCoordsPerVertVerts; + static final int kTotalBufferSizeCoordsTex = kQuadsPerBuffer * kVertsPerQuad * kCoordsPerVertTex; + static final int kTotalBufferSizeBytesVerts = kTotalBufferSizeCoordsVerts * 4; + static final int kTotalBufferSizeBytesTex = kTotalBufferSizeCoordsTex * 4; + static final int kSizeInBytes_OneVertices_VertexData = kCoordsPerVertVerts * 4; + static final int kSizeInBytes_OneVertices_TexData = kCoordsPerVertTex * 4; + private final Font font; + private final boolean antialiased; + private final boolean useFractionalMetrics; + private final GL2 gl; + + // Whether we're attempting to use automatic mipmap generation support + private boolean mipmap; + private RectanglePacker packer; + private boolean haveMaxSize; + private final RenderDelegate renderDelegate; + private TextureRenderer cachedBackingStore; + private Graphics2D cachedGraphics; + private FontRenderContext cachedFontRenderContext; + private final Map stringLocations = new HashMap(); + private final GlyphProducer mGlyphProducer; + + private int numRenderCycles; + + // Need to keep track of whether we're in a beginRendering() / + // endRendering() cycle so we can re-enter the exact same state if + // we have to reallocate the backing store + private boolean inBeginEndPair; + + // For resetting the color after disposal of the old backing store + private boolean haveCachedColor; + private float cachedR; + private float cachedG; + private float cachedB; + private float cachedA; + private Color cachedColor; + private boolean needToResetColor; + + // For debugging only + private Frame dbgFrame; + + // Debugging purposes only + private boolean debugged; + Pipelined_QuadRenderer mPipelinedQuadRenderer; + + // Whether GL_LINEAR filtering is enabled for the backing store + private boolean smoothing = true; + + /** Creates a new TextRenderer with the given font, using no + antialiasing or fractional metrics, and the default + RenderDelegate. Equivalent to TextRenderer(font, false, + false). + + @param font the font to render with + */ + public TextRenderer(final GL2 gl, final Font font) { + this(gl, font, false, false, null, false); + } + + /** Creates a new TextRenderer with the given font, using no + antialiasing or fractional metrics, and the default + RenderDelegate. If mipmap is true, attempts to use + OpenGL's automatic mipmap generation for better smoothing when + rendering the TextureRenderer's contents at a distance. + Equivalent to TextRenderer(font, false, false). + + @param font the font to render with + @param mipmap whether to attempt use of automatic mipmap generation + */ + public TextRenderer(final GL2 gl, final Font font, final boolean mipmap) { + this(gl, font, false, false, null, mipmap); + } + + /** Creates a new TextRenderer with the given Font, specified font + properties, and default RenderDelegate. The + antialiased and useFractionalMetrics + flags provide control over the same properties at the Java 2D + level. No mipmap support is requested. Equivalent to + TextRenderer(font, antialiased, useFractionalMetrics, + null). + + @param font the font to render with + @param antialiased whether to use antialiased fonts + @param useFractionalMetrics whether to use fractional font + metrics at the Java 2D level + */ + public TextRenderer(final GL2 gl, final Font font, final boolean antialiased, + final boolean useFractionalMetrics) { + this(gl, font, antialiased, useFractionalMetrics, null, false); + } + + /** Creates a new TextRenderer with the given Font, specified font + properties, and given RenderDelegate. The + antialiased and useFractionalMetrics + flags provide control over the same properties at the Java 2D + level. The renderDelegate provides more control + over the text rendered. No mipmap support is requested. + + @param font the font to render with + @param antialiased whether to use antialiased fonts + @param useFractionalMetrics whether to use fractional font + metrics at the Java 2D level + @param renderDelegate the render delegate to use to draw the + text's bitmap, or null to use the default one + */ + public TextRenderer(final GL2 gl, final Font font, final boolean antialiased, + final boolean useFractionalMetrics, final RenderDelegate renderDelegate) { + this(gl, font, antialiased, useFractionalMetrics, renderDelegate, false); + } + + /** Creates a new TextRenderer with the given Font, specified font + properties, and given RenderDelegate. The + antialiased and useFractionalMetrics + flags provide control over the same properties at the Java 2D + level. The renderDelegate provides more control + over the text rendered. If mipmap is true, attempts + to use OpenGL's automatic mipmap generation for better smoothing + when rendering the TextureRenderer's contents at a distance. + + @param font the font to render with + @param antialiased whether to use antialiased fonts + @param useFractionalMetrics whether to use fractional font + metrics at the Java 2D level + @param renderDelegate the render delegate to use to draw the + text's bitmap, or null to use the default one + @param mipmap whether to attempt use of automatic mipmap generation + */ + public TextRenderer(final GL2 gl, final Font font, final boolean antialiased, + final boolean useFractionalMetrics, RenderDelegate renderDelegate, + final boolean mipmap) { + this.gl = gl; + this.font = font; + this.antialiased = antialiased; + this.useFractionalMetrics = useFractionalMetrics; + this.mipmap = mipmap; + + // FIXME: consider adjusting the size based on font size + // (it will already automatically resize if necessary) + packer = new RectanglePacker(new Manager(), kSize, kSize); + + if (renderDelegate == null) { + renderDelegate = new DefaultRenderDelegate(); + } + + this.renderDelegate = renderDelegate; + + mGlyphProducer = new GlyphProducer(font.getNumGlyphs()); + } + + /** Returns the bounding rectangle of the given String, assuming it + was rendered at the origin. See {@link #getBounds(CharSequence) + getBounds(CharSequence)}. */ + public Rectangle2D getBounds(final String str) { + return getBounds((CharSequence) str); + } + + /** Returns the bounding rectangle of the given CharSequence, + assuming it was rendered at the origin. The coordinate system of + the returned rectangle is Java 2D's, with increasing Y + coordinates in the downward direction. The relative coordinate + (0, 0) in the returned rectangle corresponds to the baseline of + the leftmost character of the rendered string, in similar + fashion to the results returned by, for example, {@link + GlyphVector#getVisualBounds}. Most applications + will use only the width and height of the returned Rectangle for + the purposes of centering or justifying the String. It is not + specified which Java 2D bounds ({@link + GlyphVector#getVisualBounds getVisualBounds}, + {@link GlyphVector#getPixelBounds getPixelBounds}, + etc.) the returned bounds correspond to, although every effort + is made to ensure an accurate bound. */ + public Rectangle2D getBounds(final CharSequence str) { + // FIXME: this should be more optimized and use the glyph cache + final Rect r = stringLocations.get(str); + + if (r != null) { + final TextData data = (TextData) r.getUserData(); + + // Reconstitute the Java 2D results based on the cached values + return new Rectangle2D.Double(-data.origin().x, -data.origin().y, + r.w(), r.h()); + } + + // Must return a Rectangle compatible with the layout algorithm -- + // must be idempotent + return normalize(renderDelegate.getBounds(str, font, + getFontRenderContext())); + } + + /** Returns the Font this renderer is using. */ + public Font getFont() { + return font; + } + + /** Returns a FontRenderContext which can be used for external + text-related size computations. This object should be considered + transient and may become invalidated between {@link + #beginRendering beginRendering} / {@link #endRendering + endRendering} pairs. */ + public FontRenderContext getFontRenderContext() { + if (cachedFontRenderContext == null) { + cachedFontRenderContext = getGraphics2D().getFontRenderContext(); + } + + return cachedFontRenderContext; + } + + /** Begins rendering of 2D text in 3D with this {@link TextRenderer + TextRenderer} into the current OpenGL drawable. Assumes the end + user is responsible for setting up the modelview and projection + matrices, and will render text using the {@link #draw3D draw3D} + method. This method pushes some OpenGL state bits, binds and + enables the internal OpenGL texture object, sets the texture + environment mode to GL_MODULATE, and changes the current color + to the last color set with this TextRenderer via {@link + #setColor setColor}. + + + */ + public void begin3DRendering() { + beginRendering(false, 0, 0, false); + } + + /** Changes the current color of this TextRenderer to the supplied + one. The default color is opaque white. + + @param color the new color to use for rendering text + + */ + public void setColor(final Color color) { + final boolean noNeedForFlush = (haveCachedColor && (cachedColor != null) && + color.equals(cachedColor)); + + if (!noNeedForFlush) { + flushGlyphPipeline(); + } + + getBackingStore().setColor(color); + haveCachedColor = true; + cachedColor = color; + } + + /** Changes the current color of this TextRenderer to the supplied + one, where each component ranges from 0.0f - 1.0f. The alpha + component, if used, does not need to be premultiplied into the + color channels as described in the documentation for {@link + com.jogamp.opengl.util.texture.Texture Texture}, although + premultiplied colors are used internally. The default color is + opaque white. + + @param r the red component of the new color + @param g the green component of the new color + @param b the blue component of the new color + @param a the alpha component of the new color, 0.0f = completely + transparent, 1.0f = completely opaque + + */ + public void setColor(final float r, final float g, final float b, final float a) + { + final boolean noNeedForFlush = (haveCachedColor && (cachedColor == null) && + (r == cachedR) && (g == cachedG) && (b == cachedB) && + (a == cachedA)); + + if (!noNeedForFlush) { + flushGlyphPipeline(); + } + + getBackingStore().setColor(r, g, b, a); + haveCachedColor = true; + cachedR = r; + cachedG = g; + cachedB = b; + cachedA = a; + cachedColor = null; + } + + /** Draws the supplied CharSequence at the desired location using + the renderer's current color. The baseline of the leftmost + character is at position (x, y) specified in OpenGL coordinates, + where the origin is at the lower-left of the drawable and the Y + coordinate increases in the upward direction. + + @param str the string to draw + @param x the x coordinate at which to draw + @param y the y coordinate at which to draw + + */ + public void draw(final CharSequence str, final int x, final int y) { + draw3D(str, x, y, 0, 1); + } + + /** Draws the supplied String at the desired location using the + renderer's current color. See {@link #draw(CharSequence, int, + int) draw(CharSequence, int, int)}. */ + public void draw(final String str, final int x, final int y) { + draw3D(str, x, y, 0, 1); + } + + /** Draws the supplied CharSequence at the desired 3D location using + the renderer's current color. The baseline of the leftmost + character is placed at position (x, y, z) in the current + coordinate system. + + @param str the string to draw + @param x the x coordinate at which to draw + @param y the y coordinate at which to draw + @param z the z coordinate at which to draw + @param scaleFactor a uniform scale factor applied to the width and height of the drawn rectangle + + */ + public void draw3D(final CharSequence str, final float x, final float y, final float z, + final float scaleFactor) { + internal_draw3D(str, x, y, z, scaleFactor); + } + + /** Draws the supplied String at the desired 3D location using the + renderer's current color. See {@link #draw3D(CharSequence, + float, float, float, float) draw3D(CharSequence, float, float, + float, float)}. */ + public void draw3D(final String str, final float x, final float y, final float z, final float scaleFactor) { + internal_draw3D(str, x, y, z, scaleFactor); + } + + /** Returns the pixel width of the given character. */ + public float getCharWidth(final char inChar) { + return mGlyphProducer.getGlyphPixelWidth(inChar); + } + + /** Causes the TextRenderer to flush any internal caches it may be + maintaining and draw its rendering results to the screen. This + should be called after each call to draw() if you are setting + OpenGL state such as the modelview matrix between calls to + draw(). */ + public void flush() { + flushGlyphPipeline(); + } + + /** Ends a render cycle with this {@link TextRenderer TextRenderer}. + Restores the projection and modelview matrices as well as + several OpenGL state bits. Should be paired with {@link + #beginRendering beginRendering}. + + + */ + public void endRendering() { + endRendering(true); + } + + /** Ends a 3D render cycle with this {@link TextRenderer TextRenderer}. + Restores several OpenGL state bits. Should be paired with {@link + #begin3DRendering begin3DRendering}. + + + */ + public void end3DRendering() { + endRendering(false); + } + + /** Disposes of all resources this TextRenderer is using. It is not + valid to use the TextRenderer after this method is called. + + + */ + public void dispose() { + if( null != mPipelinedQuadRenderer ) { + mPipelinedQuadRenderer.dispose(); + } + packer.dispose(); + packer = null; + cachedBackingStore = null; + cachedGraphics = null; + cachedFontRenderContext = null; + + if (dbgFrame != null) { + dbgFrame.dispose(); + } + } + + //---------------------------------------------------------------------- + // Internals only below this point + // + + private static Rectangle2D preNormalize(final Rectangle2D src) { + // Need to round to integer coordinates + // Also give ourselves a little slop around the reported + // bounds of glyphs because it looks like neither the visual + // nor the pixel bounds works perfectly well + final int minX = (int) Math.floor(src.getMinX()) - 1; + final int minY = (int) Math.floor(src.getMinY()) - 1; + final int maxX = (int) Math.ceil(src.getMaxX()) + 1; + final int maxY = (int) Math.ceil(src.getMaxY()) + 1; + return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); + } + + + private Rectangle2D normalize(final Rectangle2D src) { + // Give ourselves a boundary around each entity on the backing + // store in order to prevent bleeding of nearby Strings due to + // the fact that we use linear filtering + + // NOTE that this boundary is quite heuristic and is related + // to how far away in 3D we may view the text -- + // heuristically, 1.5% of the font's height + final int boundary = (int) Math.max(1, 0.015 * font.getSize()); + + return new Rectangle2D.Double((int) Math.floor(src.getMinX() - boundary), + (int) Math.floor(src.getMinY() - boundary), + (int) Math.ceil(src.getWidth() + 2 * boundary), + (int) Math.ceil(src.getHeight()) + 2 * boundary); + } + + private TextureRenderer getBackingStore() { + final TextureRenderer renderer = (TextureRenderer) packer.getBackingStore(); + + if (renderer != cachedBackingStore) { + // Backing store changed since last time; discard any cached Graphics2D + if (cachedGraphics != null) { + cachedGraphics.dispose(); + cachedGraphics = null; + cachedFontRenderContext = null; + } + + cachedBackingStore = renderer; + } + + return cachedBackingStore; + } + + private Graphics2D getGraphics2D() { + final TextureRenderer renderer = getBackingStore(); + + if (cachedGraphics == null) { + cachedGraphics = renderer.createGraphics(); + + // Set up composite, font and rendering hints + cachedGraphics.setComposite(AlphaComposite.Src); + cachedGraphics.setColor(Color.WHITE); + cachedGraphics.setFont(font); + cachedGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + (antialiased ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON + : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)); + cachedGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, + (useFractionalMetrics + ? RenderingHints.VALUE_FRACTIONALMETRICS_ON + : RenderingHints.VALUE_FRACTIONALMETRICS_OFF)); + } + + return cachedGraphics; + } + + private void beginRendering(final boolean ortho, final int width, final int height, + final boolean disableDepthTestForOrtho) { + + assert !ortho; + + inBeginEndPair = true; + + getBackingStore().begin3DRendering(); + + // Push client attrib bits used by the pipelined quad renderer + gl.glPushClientAttrib((int) gl.GL_ALL_CLIENT_ATTRIB_BITS()); + + if (!haveMaxSize) { + // Query OpenGL for the maximum texture size and set it in the + // RectanglePacker to keep it from expanding too large + final int[] sz = new int[1]; + gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE(), sz); + packer.setMaxSize(sz[0], sz[0]); + haveMaxSize = true; + } + + if (needToResetColor && haveCachedColor) { + if (cachedColor == null) { + getBackingStore().setColor(cachedR, cachedG, cachedB, cachedA); + } else { + getBackingStore().setColor(cachedColor); + } + + needToResetColor = false; + } + + // Disable future attempts to use mipmapping if TextureRenderer + // doesn't support it + if (mipmap && !getBackingStore().isUsingAutoMipmapGeneration()) { + mipmap = false; + } + } + + /** + * emzic: here the call to glBindBuffer crashes on certain graphicscard/driver combinations + * this is why the ugly try-catch block has been added, which falls back to the old textrenderer + * + * @param ortho + + */ + private void endRendering(final boolean ortho) { + assert !ortho; + + flushGlyphPipeline(); + + inBeginEndPair = false; + + // Pop client attrib bits used by the pipelined quad renderer + gl.glPopClientAttrib(); + + // The OpenGL spec is unclear about whether this changes the + // buffer bindings, so preemptively zero out the GL_ARRAY_BUFFER + // binding + gl.glBindBuffer(gl.GL_ARRAY_BUFFER(), 0); + + getBackingStore().end3DRendering(); + + if (++numRenderCycles >= CYCLES_PER_FLUSH) { + numRenderCycles = 0; + + clearUnusedEntries(); + } + } + + private void clearUnusedEntries() { + final List deadRects = new ArrayList(); + + // Iterate through the contents of the backing store, removing + // text strings that haven't been used recently + packer.visit(new RectVisitor() { + @Override + public void visit(final Rect rect) { + final TextData data = (TextData) rect.getUserData(); + + if (data.used()) { + data.clearUsed(); + } else { + deadRects.add(rect); + } + } + }); + + for (final Rect r : deadRects) { + packer.remove(r); + stringLocations.remove(((TextData) r.getUserData()).string()); + + final int unicodeToClearFromCache = ((TextData) r.getUserData()).unicodeID; + + if (unicodeToClearFromCache > 0) { + mGlyphProducer.clearCacheEntry(unicodeToClearFromCache); + } + + } + + // If we removed dead rectangles this cycle, try to do a compaction + final float frag = packer.verticalFragmentationRatio(); + + if (!deadRects.isEmpty() && (frag > MAX_VERTICAL_FRAGMENTATION)) { + + packer.compact(); + } + + } + + private void internal_draw3D(final CharSequence str, float x, final float y, final float z, + final float scaleFactor) { + for (final Glyph glyph : mGlyphProducer.getGlyphs(str)) { + final float advance = glyph.draw3D(x, y, z, scaleFactor); + x += advance * scaleFactor; + } + } + + private void flushGlyphPipeline() { + if (mPipelinedQuadRenderer != null) { + mPipelinedQuadRenderer.draw(); + } + } + + private void draw3D_ROBUST(final CharSequence str, final float x, final float y, final float z, + final float scaleFactor) { + String curStr; + if (str instanceof String) { + curStr = (String) str; + } else { + curStr = str.toString(); + } + + // Look up the string on the backing store + Rect rect = stringLocations.get(curStr); + + if (rect == null) { + // Rasterize this string and place it on the backing store + Graphics2D g = getGraphics2D(); + final Rectangle2D origBBox = preNormalize(renderDelegate.getBounds(curStr, font, getFontRenderContext())); + final Rectangle2D bbox = normalize(origBBox); + final Point origin = new Point((int) -bbox.getMinX(), + (int) -bbox.getMinY()); + rect = new Rect(0, 0, (int) bbox.getWidth(), + (int) bbox.getHeight(), + new TextData(curStr, origin, origBBox, -1)); + + packer.add(rect); + stringLocations.put(curStr, rect); + + // Re-fetch the Graphics2D in case the addition of the rectangle + // caused the old backing store to be thrown away + g = getGraphics2D(); + + // OK, should now have an (x, y) for this rectangle; rasterize + // the String + final int strx = rect.x() + origin.x; + final int stry = rect.y() + origin.y; + + // Clear out the area we're going to draw into + g.setComposite(AlphaComposite.Clear); + g.fillRect(rect.x(), rect.y(), rect.w(), rect.h()); + g.setComposite(AlphaComposite.Src); + + // Draw the string + renderDelegate.draw(g, curStr, strx, stry); + + // Mark this region of the TextureRenderer as dirty + getBackingStore().markDirty(rect.x(), rect.y(), rect.w(), + rect.h()); + } + + // OK, now draw the portion of the backing store to the screen + final TextureRenderer renderer = getBackingStore(); + + // NOTE that the rectangles managed by the packer have their + // origin at the upper-left but the TextureRenderer's origin is + // at its lower left!!! + final TextData data = (TextData) rect.getUserData(); + data.markUsed(); + + final Rectangle2D origRect = data.origRect(); + + // Align the leftmost point of the baseline to the (x, y, z) coordinate requested + renderer.draw3DRect(x - (scaleFactor * data.origOriginX()), + y - (scaleFactor * ((float) origRect.getHeight() - data.origOriginY())), z, + rect.x() + (data.origin().x - data.origOriginX()), + renderer.getHeight() - rect.y() - (int) origRect.getHeight() - + (data.origin().y - data.origOriginY()), + (int) origRect.getWidth(), (int) origRect.getHeight(), scaleFactor); + } + + /** Class supporting more full control over the process of rendering + the bitmapped text. Allows customization of whether the backing + store text bitmap is full-color or intensity only, the size of + each individual rendered text rectangle, and the contents of + each individual rendered text string. The default implementation + of this interface uses an intensity-only texture, a + closely-cropped rectangle around the text, and renders text + using the color white, which is modulated by the set color + during the rendering process. */ + public static interface RenderDelegate { + /** Indicates whether the backing store of this TextRenderer + should be intensity-only (the default) or full-color. */ + public boolean intensityOnly(); + + /** Computes the bounds of the given String relative to the + origin. */ + public Rectangle2D getBounds(String str, Font font, + FontRenderContext frc); + + /** Computes the bounds of the given character sequence relative + to the origin. */ + public Rectangle2D getBounds(CharSequence str, Font font, + FontRenderContext frc); + + /** Computes the bounds of the given GlyphVector, already + assumed to have been created for a particular Font, + relative to the origin. */ + public Rectangle2D getBounds(GlyphVector gv, FontRenderContext frc); + + /** Render the passed character sequence at the designated + location using the supplied Graphics2D instance. The + surrounding region will already have been cleared to the RGB + color (0, 0, 0) with zero alpha. The initial drawing context + of the passed Graphics2D will be set to use + AlphaComposite.Src, the color white, the Font specified in the + TextRenderer's constructor, and the rendering hints specified + in the TextRenderer constructor. Changes made by the end user + may be visible in successive calls to this method, but are not + guaranteed to be preserved. Implementors of this method + should reset the Graphics2D's state to that desired each time + this method is called, in particular those states which are + not the defaults. */ + public void draw(Graphics2D graphics, String str, int x, int y); + + /** Render the passed GlyphVector at the designated location using + the supplied Graphics2D instance. The surrounding region will + already have been cleared to the RGB color (0, 0, 0) with zero + alpha. The initial drawing context of the passed Graphics2D + will be set to use AlphaComposite.Src, the color white, the + Font specified in the TextRenderer's constructor, and the + rendering hints specified in the TextRenderer constructor. + Changes made by the end user may be visible in successive + calls to this method, but are not guaranteed to be preserved. + Implementors of this method should reset the Graphics2D's + state to that desired each time this method is called, in + particular those states which are not the defaults. */ + public void drawGlyphVector(Graphics2D graphics, GlyphVector str, + int x, int y); + } + + private static class CharSequenceIterator implements CharacterIterator { + CharSequence mSequence; + int mLength; + int mCurrentIndex; + + CharSequenceIterator() { + } + + CharSequenceIterator(final CharSequence sequence) { + initFromCharSequence(sequence); + } + + public void initFromCharSequence(final CharSequence sequence) { + mSequence = sequence; + mLength = mSequence.length(); + mCurrentIndex = 0; + } + + @Override + public char last() { + mCurrentIndex = Math.max(0, mLength - 1); + + return current(); + } + + @Override + public char current() { + if ((mLength == 0) || (mCurrentIndex >= mLength)) { + return CharacterIterator.DONE; + } + + return mSequence.charAt(mCurrentIndex); + } + + @Override + public char next() { + mCurrentIndex++; + + return current(); + } + + @Override + public char previous() { + mCurrentIndex = Math.max(mCurrentIndex - 1, 0); + + return current(); + } + + @Override + public char setIndex(final int position) { + mCurrentIndex = position; + + return current(); + } + + @Override + public int getBeginIndex() { + return 0; + } + + @Override + public int getEndIndex() { + return mLength; + } + + @Override + public int getIndex() { + return mCurrentIndex; + } + + @Override + public Object clone() { + final CharSequenceIterator iter = new CharSequenceIterator(mSequence); + iter.mCurrentIndex = mCurrentIndex; + + return iter; + } + + @Override + public char first() { + if (mLength == 0) { + return CharacterIterator.DONE; + } + + mCurrentIndex = 0; + + return current(); + } + } + + // Data associated with each rectangle of text + static class TextData { + // Back-pointer to String this TextData describes, if it + // represents a String rather than a single glyph + private final String str; + + // If this TextData represents a single glyph, this is its + // unicode ID + int unicodeID; + + // The following must be defined and used VERY precisely. This is + // the offset from the upper-left corner of this rectangle (Java + // 2D coordinate system) at which the string must be rasterized in + // order to fit within the rectangle -- the leftmost point of the + // baseline. + private final Point origin; + + // This represents the pre-normalized rectangle, which fits + // within the rectangle on the backing store. We keep a + // one-pixel border around entries on the backing store to + // prevent bleeding of adjacent letters when using GL_LINEAR + // filtering for rendering. The origin of this rectangle is + // equivalent to the origin above. + private final Rectangle2D origRect; + + private boolean used; // Whether this text was used recently + + TextData(final String str, final Point origin, final Rectangle2D origRect, final int unicodeID) { + this.str = str; + this.origin = origin; + this.origRect = origRect; + this.unicodeID = unicodeID; + } + + String string() { + return str; + } + + Point origin() { + return origin; + } + + // The following three methods are used to locate the glyph + // within the expanded rectangle coming from normalize() + int origOriginX() { + return (int) -origRect.getMinX(); + } + + int origOriginY() { + return (int) -origRect.getMinY(); + } + + Rectangle2D origRect() { + return origRect; + } + + boolean used() { + return used; + } + + void markUsed() { + used = true; + } + + void clearUsed() { + used = false; + } + } + + class Manager implements BackingStoreManager { + private Graphics2D g; + + @Override + public Object allocateBackingStore(final int w, final int h) { + // FIXME: should consider checking Font's attributes to see + // whether we're likely to need to support a full RGBA backing + // store (i.e., non-default Paint, foreground color, etc.), but + // for now, let's just be more efficient + TextureRenderer renderer; + + if (renderDelegate.intensityOnly()) { + renderer = TextureRenderer.createAlphaOnlyRenderer(gl, w, h, mipmap); + } else { + renderer = new TextureRenderer(gl, w, h, true, mipmap); + } + renderer.setSmoothing(smoothing); + + return renderer; + } + + @Override + public void deleteBackingStore(final Object backingStore) { + ((TextureRenderer) backingStore).dispose(); + } + + @Override + public boolean preExpand(final Rect cause, final int attemptNumber) { + // Only try this one time; clear out potentially obsolete entries + // NOTE: this heuristic and the fact that it clears the used bit + // of all entries seems to cause cycling of entries in some + // situations, where the backing store becomes small compared to + // the amount of text on the screen (see the TextFlow demo) and + // the entries continually cycle in and out of the backing + // store, decreasing performance. If we added a little age + // information to the entries, and only cleared out entries + // above a certain age, this behavior would be eliminated. + // However, it seems the system usually stabilizes itself, so + // for now we'll just keep things simple. Note that if we don't + // clear the used bit here, the backing store tends to increase + // very quickly to its maximum size, at least with the TextFlow + // demo when the text is being continually re-laid out. + if (attemptNumber == 0) { + if (inBeginEndPair) { + // Draw any outstanding glyphs + flush(); + } + + clearUnusedEntries(); + + return true; + } + + return false; + } + + @Override + public boolean additionFailed(final Rect cause, final int attemptNumber) { + // Heavy hammer -- might consider doing something different + packer.clear(); + stringLocations.clear(); + mGlyphProducer.clearAllCacheEntries(); + + if (attemptNumber == 0) { + return true; + } + + return false; + } + + @Override + public boolean canCompact() { + return true; + } + + @Override + public void beginMovement(final Object oldBackingStore, final Object newBackingStore) { + // Exit the begin / end pair if necessary + if (inBeginEndPair) { + // Draw any outstanding glyphs + flush(); + + // Pop client attrib bits used by the pipelined quad renderer + gl.glPopClientAttrib(); + + // The OpenGL spec is unclear about whether this changes the + // buffer bindings, so preemptively zero out the GL_ARRAY_BUFFER + // binding + gl.glBindBuffer(gl.GL_ARRAY_BUFFER(), 0); + + ((TextureRenderer) oldBackingStore).end3DRendering(); + } + + final TextureRenderer newRenderer = (TextureRenderer) newBackingStore; + g = newRenderer.createGraphics(); + } + + @Override + public void move(final Object oldBackingStore, final Rect oldLocation, + final Object newBackingStore, final Rect newLocation) { + final TextureRenderer oldRenderer = (TextureRenderer) oldBackingStore; + final TextureRenderer newRenderer = (TextureRenderer) newBackingStore; + + if (oldRenderer == newRenderer) { + // Movement on the same backing store -- easy case + g.copyArea(oldLocation.x(), oldLocation.y(), oldLocation.w(), + oldLocation.h(), newLocation.x() - oldLocation.x(), + newLocation.y() - oldLocation.y()); + } else { + // Need to draw from the old renderer's image into the new one + final Image img = oldRenderer.getImage(); + g.drawImage(img, newLocation.x(), newLocation.y(), + newLocation.x() + newLocation.w(), + newLocation.y() + newLocation.h(), oldLocation.x(), + oldLocation.y(), oldLocation.x() + oldLocation.w(), + oldLocation.y() + oldLocation.h(), null); + } + } + + @Override + public void endMovement(final Object oldBackingStore, final Object newBackingStore) { + g.dispose(); + + // Sync the whole surface + final TextureRenderer newRenderer = (TextureRenderer) newBackingStore; + newRenderer.markDirty(0, 0, newRenderer.getWidth(), + newRenderer.getHeight()); + + // Re-enter the begin / end pair if necessary + if (inBeginEndPair) { + ((TextureRenderer) newBackingStore).begin3DRendering(); + + // Push client attrib bits used by the pipelined quad renderer + gl.glPushClientAttrib((int)gl.GL_ALL_CLIENT_ATTRIB_BITS()); + + if (haveCachedColor) { + if (cachedColor == null) { + ((TextureRenderer) newBackingStore).setColor(cachedR, + cachedG, cachedB, cachedA); + } else { + ((TextureRenderer) newBackingStore).setColor(cachedColor); + } + } + } else { + needToResetColor = true; + } + } + } + + public static class DefaultRenderDelegate implements RenderDelegate { + @Override + public boolean intensityOnly() { + return true; + } + + @Override + public Rectangle2D getBounds(final CharSequence str, final Font font, + final FontRenderContext frc) { + return getBounds(font.createGlyphVector(frc, + new CharSequenceIterator(str)), + frc); + } + + @Override + public Rectangle2D getBounds(final String str, final Font font, + final FontRenderContext frc) { + return getBounds(font.createGlyphVector(frc, str), frc); + } + + @Override + public Rectangle2D getBounds(final GlyphVector gv, final FontRenderContext frc) { + return gv.getVisualBounds(); + } + + @Override + public void drawGlyphVector(final Graphics2D graphics, final GlyphVector str, + final int x, final int y) { + graphics.drawGlyphVector(str, x, y); + } + + @Override + public void draw(final Graphics2D graphics, final String str, final int x, final int y) { + graphics.drawString(str, x, y); + } + } + + //---------------------------------------------------------------------- + // Glyph-by-glyph rendering support + // + + // A temporary to prevent excessive garbage creation + private final char[] singleUnicode = new char[1]; + + /** A Glyph represents either a single unicode glyph or a + substring of characters to be drawn. The reason for the dual + behavior is so that we can take in a sequence of unicode + characters and partition them into runs of individual glyphs, + but if we encounter complex text and/or unicode sequences we + don't understand, we can render them using the + string-by-string method.

+ + Glyphs need to be able to re-upload themselves to the backing + store on demand as we go along in the render sequence. + */ + + class Glyph { + // If this Glyph represents an individual unicode glyph, this + // is its unicode ID. If it represents a String, this is -1. + private int unicodeID; + // If the above field isn't -1, then these fields are used. + // The glyph code in the font + private int glyphCode; + // The GlyphProducer which created us + private GlyphProducer producer; + // The advance of this glyph + private float advance; + // The GlyphVector for this single character; this is passed + // in during construction but cleared during the upload + // process + private GlyphVector singleUnicodeGlyphVector; + // The rectangle of this glyph on the backing store, or null + // if it has been cleared due to space pressure + private Rect glyphRectForTextureMapping; + // If this Glyph represents a String, this is the sequence of + // characters + private String str; + // Whether we need a valid advance when rendering this string + // (i.e., whether it has other single glyphs coming after it) + private boolean needAdvance; + + // Creates a Glyph representing an individual Unicode character + public Glyph(final int unicodeID, + final int glyphCode, + final float advance, + final GlyphVector singleUnicodeGlyphVector, + final GlyphProducer producer) { + this.unicodeID = unicodeID; + this.glyphCode = glyphCode; + this.advance = advance; + this.singleUnicodeGlyphVector = singleUnicodeGlyphVector; + this.producer = producer; + } + + // Creates a Glyph representing a sequence of characters, with + // an indication of whether additional single glyphs are being + // rendered after it + public Glyph(final String str, final boolean needAdvance) { + this.str = str; + this.needAdvance = needAdvance; + } + + /** Returns this glyph's unicode ID */ + public int getUnicodeID() { + return unicodeID; + } + + /** Returns this glyph's (font-specific) glyph code */ + public int getGlyphCode() { + return glyphCode; + } + + /** Returns the advance for this glyph */ + public float getAdvance() { + return advance; + } + + /** Draws this glyph and returns the (x) advance for this glyph */ + public float draw3D(final float inX, final float inY, final float z, final float scaleFactor) { + if (str != null) { + draw3D_ROBUST(str, inX, inY, z, scaleFactor); + if (!needAdvance) { + return 0; + } + // Compute and return the advance for this string + final GlyphVector gv = font.createGlyphVector(getFontRenderContext(), str); + float totalAdvance = 0; + for (int i = 0; i < gv.getNumGlyphs(); i++) { + totalAdvance += gv.getGlyphMetrics(i).getAdvance(); + } + return totalAdvance; + } + + // This is the code path taken for individual glyphs + if (glyphRectForTextureMapping == null) { + upload(); + } + + try { + if (mPipelinedQuadRenderer == null) { + mPipelinedQuadRenderer = new Pipelined_QuadRenderer(); + } + + final TextureRenderer renderer = getBackingStore(); + // Handles case where NPOT texture is used for backing store + final TextureCoords wholeImageTexCoords = renderer.getTexture().getImageTexCoords(); + final float xScale = wholeImageTexCoords.right(); + final float yScale = wholeImageTexCoords.bottom(); + + final Rect rect = glyphRectForTextureMapping; + final TextData data = (TextData) rect.getUserData(); + data.markUsed(); + + final Rectangle2D origRect = data.origRect(); + + final float x = inX - (scaleFactor * data.origOriginX()); + final float y = inY - (scaleFactor * ((float) origRect.getHeight() - data.origOriginY())); + + final int texturex = rect.x() + (data.origin().x - data.origOriginX()); + final int texturey = renderer.getHeight() - rect.y() - (int) origRect.getHeight() - + (data.origin().y - data.origOriginY()); + final int width = (int) origRect.getWidth(); + final int height = (int) origRect.getHeight(); + + final float tx1 = xScale * texturex / renderer.getWidth(); + final float ty1 = yScale * (1.0f - + ((float) texturey / (float) renderer.getHeight())); + final float tx2 = xScale * (texturex + width) / renderer.getWidth(); + final float ty2 = yScale * (1.0f - + ((float) (texturey + height) / (float) renderer.getHeight())); + + mPipelinedQuadRenderer.glTexCoord2f(tx1, ty1); + mPipelinedQuadRenderer.glVertex3f(x, y, z); + mPipelinedQuadRenderer.glTexCoord2f(tx2, ty1); + mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor), y, + z); + mPipelinedQuadRenderer.glTexCoord2f(tx2, ty2); + mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor), + y + (height * scaleFactor), z); + mPipelinedQuadRenderer.glTexCoord2f(tx1, ty2); + mPipelinedQuadRenderer.glVertex3f(x, + y + (height * scaleFactor), z); + } catch (final Exception e) { + e.printStackTrace(); + } + return advance; + } + + /** Notifies this glyph that it's been cleared out of the cache */ + public void clear() { + glyphRectForTextureMapping = null; + } + + private void upload() { + final GlyphVector gv = getGlyphVector(); + final Rectangle2D origBBox = preNormalize(renderDelegate.getBounds(gv, getFontRenderContext())); + final Rectangle2D bbox = normalize(origBBox); + final Point origin = new Point((int) -bbox.getMinX(), + (int) -bbox.getMinY()); + final Rect rect = new Rect(0, 0, (int) bbox.getWidth(), + (int) bbox.getHeight(), + new TextData(null, origin, origBBox, unicodeID)); + packer.add(rect); + glyphRectForTextureMapping = rect; + final Graphics2D g = getGraphics2D(); + // OK, should now have an (x, y) for this rectangle; rasterize + // the glyph + final int strx = rect.x() + origin.x; + final int stry = rect.y() + origin.y; + + // Clear out the area we're going to draw into + g.setComposite(AlphaComposite.Clear); + g.fillRect(rect.x(), rect.y(), rect.w(), rect.h()); + g.setComposite(AlphaComposite.Src); + + // Draw the string + renderDelegate.drawGlyphVector(g, gv, strx, stry); + + if (false) { + final TextData data = (TextData) rect.getUserData(); + // Draw a bounding box on the backing store + g.drawRect(strx - data.origOriginX(), + stry - data.origOriginY(), + (int) data.origRect().getWidth(), + (int) data.origRect().getHeight()); + g.drawRect(strx - data.origin().x, + stry - data.origin().y, + rect.w(), + rect.h()); + } + + // Mark this region of the TextureRenderer as dirty + getBackingStore().markDirty(rect.x(), rect.y(), rect.w(), + rect.h()); + // Re-register ourselves with our producer + producer.register(this); + } + + private GlyphVector getGlyphVector() { + final GlyphVector gv = singleUnicodeGlyphVector; + if (gv != null) { + singleUnicodeGlyphVector = null; // Don't need this anymore + return gv; + } + singleUnicode[0] = (char) unicodeID; + return font.createGlyphVector(getFontRenderContext(), singleUnicode); + } + } + + class GlyphProducer { + static final int undefined = -2; + final FontRenderContext fontRenderContext = null; // FIXME: Never initialized! + List glyphsOutput = new ArrayList(); + HashMap fullGlyphVectorCache = new HashMap(); + HashMap glyphMetricsCache = new HashMap(); + // The mapping from unicode character to font-specific glyph ID + int[] unicodes2Glyphs; + // The mapping from glyph ID to Glyph + Glyph[] glyphCache; + // We re-use this for each incoming string + CharSequenceIterator iter = new CharSequenceIterator(); + + GlyphProducer(final int fontLengthInGlyphs) { + unicodes2Glyphs = new int[512]; + glyphCache = new Glyph[fontLengthInGlyphs]; + clearAllCacheEntries(); + } + + public List getGlyphs(final CharSequence inString) { + glyphsOutput.clear(); + GlyphVector fullRunGlyphVector; + fullRunGlyphVector = fullGlyphVectorCache.get(inString.toString()); + if (fullRunGlyphVector == null) { + iter.initFromCharSequence(inString); + fullRunGlyphVector = font.createGlyphVector(getFontRenderContext(), iter); + fullGlyphVectorCache.put(inString.toString(), fullRunGlyphVector); + } + final boolean complex = (fullRunGlyphVector.getLayoutFlags() != 0); + if (complex || DISABLE_GLYPH_CACHE) { + // Punt to the robust version of the renderer + glyphsOutput.add(new Glyph(inString.toString(), false)); + return glyphsOutput; + } + + final int lengthInGlyphs = fullRunGlyphVector.getNumGlyphs(); + int i = 0; + while (i < lengthInGlyphs) { + final Character letter = CharacterCache.valueOf(inString.charAt(i)); + GlyphMetrics metrics = glyphMetricsCache.get(letter); + if (metrics == null) { + metrics = fullRunGlyphVector.getGlyphMetrics(i); + glyphMetricsCache.put(letter, metrics); + } + final Glyph glyph = getGlyph(inString, metrics, i); + if (glyph != null) { + glyphsOutput.add(glyph); + i++; + } else { + // Assemble a run of characters that don't fit in + // the cache + final StringBuilder buf = new StringBuilder(); + while (i < lengthInGlyphs && + getGlyph(inString, fullRunGlyphVector.getGlyphMetrics(i), i) == null) { + buf.append(inString.charAt(i++)); + } + glyphsOutput.add(new Glyph(buf.toString(), + // Any more glyphs after this run? + i < lengthInGlyphs)); + } + } + return glyphsOutput; + } + + public void clearCacheEntry(final int unicodeID) { + final int glyphID = unicodes2Glyphs[unicodeID]; + if (glyphID != undefined) { + final Glyph glyph = glyphCache[glyphID]; + if (glyph != null) { + glyph.clear(); + } + glyphCache[glyphID] = null; + } + unicodes2Glyphs[unicodeID] = undefined; + } + + public void clearAllCacheEntries() { + for (int i = 0; i < unicodes2Glyphs.length; i++) { + clearCacheEntry(i); + } + } + + public void register(final Glyph glyph) { + unicodes2Glyphs[glyph.getUnicodeID()] = glyph.getGlyphCode(); + glyphCache[glyph.getGlyphCode()] = glyph; + } + + public float getGlyphPixelWidth(final char unicodeID) { + final Glyph glyph = getGlyph(unicodeID); + if (glyph != null) { + return glyph.getAdvance(); + } + + // Have to do this the hard / uncached way + singleUnicode[0] = unicodeID; + if( null == fontRenderContext ) { // FIXME: Never initialized! + throw new InternalError("fontRenderContext never initialized!"); + } + final GlyphVector gv = font.createGlyphVector(fontRenderContext, + singleUnicode); + return gv.getGlyphMetrics(0).getAdvance(); + } + + // Returns a glyph object for this single glyph. Returns null + // if the unicode or glyph ID would be out of bounds of the + // glyph cache. + private Glyph getGlyph(final CharSequence inString, + final GlyphMetrics glyphMetrics, + final int index) { + final char unicodeID = inString.charAt(index); + + if (unicodeID >= unicodes2Glyphs.length) { + return null; + } + + final int glyphID = unicodes2Glyphs[unicodeID]; + if (glyphID != undefined) { + return glyphCache[glyphID]; + } + + // Must fabricate the glyph + singleUnicode[0] = unicodeID; + final GlyphVector gv = font.createGlyphVector(getFontRenderContext(), singleUnicode); + return getGlyph(unicodeID, gv, glyphMetrics); + } + + // It's unclear whether this variant might produce less + // optimal results than if we can see the entire GlyphVector + // for the incoming string + private Glyph getGlyph(final int unicodeID) { + if (unicodeID >= unicodes2Glyphs.length) { + return null; + } + + final int glyphID = unicodes2Glyphs[unicodeID]; + if (glyphID != undefined) { + return glyphCache[glyphID]; + } + singleUnicode[0] = (char) unicodeID; + final GlyphVector gv = font.createGlyphVector(getFontRenderContext(), singleUnicode); + return getGlyph(unicodeID, gv, gv.getGlyphMetrics(0)); + } + + private Glyph getGlyph(final int unicodeID, + final GlyphVector singleUnicodeGlyphVector, + final GlyphMetrics metrics) { + final int glyphCode = singleUnicodeGlyphVector.getGlyphCode(0); + // Have seen huge glyph codes (65536) coming out of some fonts in some Unicode situations + if (glyphCode >= glyphCache.length) { + return null; + } + final Glyph glyph = new Glyph(unicodeID, + glyphCode, + metrics.getAdvance(), + singleUnicodeGlyphVector, + this); + register(glyph); + return glyph; + } + } + + private static class CharacterCache { + private CharacterCache() { + } + + static final Character cache[] = new Character[127 + 1]; + + static { + for (int i = 0; i < cache.length; i++) { + cache[i] = Character.valueOf((char) i); + } + } + + public static Character valueOf(final char c) { + if (c <= 127) { // must cache + return CharacterCache.cache[c]; + } + return Character.valueOf(c); + } + } + + class Pipelined_QuadRenderer { + int mOutstandingGlyphsVerticesPipeline = 0; + FloatBuffer mTexCoords; + FloatBuffer mVertCoords; + int mVBO_For_ResuableTileVertices; + int mVBO_For_ResuableTileTexCoords; + + Pipelined_QuadRenderer() { + mVertCoords = Buffers.newDirectFloatBuffer(kTotalBufferSizeCoordsVerts); + mTexCoords = Buffers.newDirectFloatBuffer(kTotalBufferSizeCoordsTex); + + final int[] vbos = new int[2]; + gl.glGenBuffers(vbos); + + mVBO_For_ResuableTileVertices = vbos[0]; + mVBO_For_ResuableTileTexCoords = vbos[1]; + + gl.glBindBuffer(gl.GL_ARRAY_BUFFER(), + mVBO_For_ResuableTileVertices); + gl.glBufferData(gl.GL_ARRAY_BUFFER(), kTotalBufferSizeBytesVerts, + null, gl.GL_STREAM_DRAW()); // stream draw because this is a single quad use pipeline + + gl.glBindBuffer(gl.GL_ARRAY_BUFFER(), + mVBO_For_ResuableTileTexCoords); + gl.glBufferData(gl.GL_ARRAY_BUFFER(), kTotalBufferSizeBytesTex, + null, gl.GL_STREAM_DRAW()); // stream draw because this is a single quad use pipeline + } + + public void glTexCoord2f(final float v, final float v1) { + mTexCoords.put(v); + mTexCoords.put(v1); + } + + public void glVertex3f(final float inX, final float inY, final float inZ) { + mVertCoords.put(inX); + mVertCoords.put(inY); + mVertCoords.put(inZ); + + mOutstandingGlyphsVerticesPipeline++; + + if (mOutstandingGlyphsVerticesPipeline >= kTotalBufferSizeVerts) { + this.draw(); + } + } + + private void draw() { + drawVertexArrays(); + } + + private void drawVertexArrays() { + if (mOutstandingGlyphsVerticesPipeline > 0) { + final TextureRenderer renderer = getBackingStore(); + renderer.getTexture(); // triggers texture uploads. Maybe this should be more obvious? + + mVertCoords.rewind(); + mTexCoords.rewind(); + + gl.glEnableClientState(gl.GL_VERTEX_ARRAY()); + + gl.glBindBuffer(gl.GL_ARRAY_BUFFER(), + mVBO_For_ResuableTileVertices); + gl.glBufferSubData(gl.GL_ARRAY_BUFFER(), 0, + mOutstandingGlyphsVerticesPipeline * kSizeInBytes_OneVertices_VertexData, + mVertCoords); // upload only the new stuff + gl.glVertexPointer(3, gl.GL_FLOAT(), 0, 0); + + gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY()); + + gl.glBindBuffer(gl.GL_ARRAY_BUFFER(), + mVBO_For_ResuableTileTexCoords); + gl.glBufferSubData(gl.GL_ARRAY_BUFFER(), 0, + mOutstandingGlyphsVerticesPipeline * kSizeInBytes_OneVertices_TexData, + mTexCoords); // upload only the new stuff + gl.glTexCoordPointer(2, gl.GL_FLOAT(), 0, 0); + + gl.glDrawArrays(gl.GL_QUADS(), 0, + mOutstandingGlyphsVerticesPipeline); + + mVertCoords.rewind(); + mTexCoords.rewind(); + mOutstandingGlyphsVerticesPipeline = 0; + } + } + + public void dispose() { + final int[] vbos = new int[2]; + vbos[0] = mVBO_For_ResuableTileVertices; + vbos[1] = mVBO_For_ResuableTileTexCoords; + gl.glDeleteBuffers(vbos); + } + } + + /** + * Sets whether smoothing (i.e., GL_LINEAR filtering) is enabled + * in the backing TextureRenderer of this TextRenderer. A few + * graphics cards do not behave well when this is enabled, + * resulting in fuzzy text. Defaults to true. + */ + public void setSmoothing(final boolean smoothing) { + this.smoothing = smoothing; + getBackingStore().setSmoothing(smoothing); + } + + /** + * Indicates whether smoothing is enabled in the backing + * TextureRenderer of this TextRenderer. A few graphics cards do + * not behave well when this is enabled, resulting in fuzzy text. + * Defaults to true. + */ + public boolean getSmoothing() { + return smoothing; + } + +} diff --git a/src/main/java/net/opengrabeso/opengl/util/awt/TextureRenderer.java b/src/main/java/net/opengrabeso/opengl/util/awt/TextureRenderer.java new file mode 100644 index 00000000..4c79f31b --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/awt/TextureRenderer.java @@ -0,0 +1,602 @@ +/* + * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. + * Copyright (c) 2010 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + * + * Sun gratefully acknowledges that this software was originally authored + * and developed by Kenneth Bradley Russell and Christopher John Kline. + */ + +package net.opengrabeso.opengl.util.awt; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.image.*; + +import com.github.opengrabeso.jaagl.GL2; +import net.opengrabeso.opengl.util.texture.*; +import net.opengrabeso.opengl.util.texture.awt.*; + +/** Provides the ability to render into an OpenGL {@link + com.jogamp.opengl.util.texture.Texture Texture} using the Java 2D + APIs. This renderer class uses an internal Java 2D image (of + unspecified type) for its backing store and flushes portions of + that image to an OpenGL texture on demand. The resulting OpenGL + texture can then be mapped on to a polygon for display. */ + +public class TextureRenderer { + // For now, we supply only a BufferedImage back-end for this + // renderer. In theory we could use the Java 2D/JOGL bridge to fully + // accelerate the rendering paths, but there are restrictions on + // what work can be done where; for example, Graphics2D-related work + // must not be done on the Queue Flusher Thread, but JOGL's + // OpenGL-related work must be. This implies that the user's code + // would need to be split up into multiple callbacks run from the + // appropriate threads, which would be somewhat unfortunate. + + private final GL2 gl; + + // Whether we're attempting to use automatic mipmap generation support + private boolean mipmap; + + // Whether smoothing is enabled for the OpenGL texture (switching + // between GL_LINEAR and GL_NEAREST filtering) + private boolean smoothing = true; + private boolean smoothingChanged; + + // The backing store itself + private BufferedImage image; + + private Texture texture; + private AWTTextureData textureData; + private boolean mustReallocateTexture; + private Rectangle dirtyRegion; + + // Current color + private float r = 1.0f; + private float g = 1.0f; + private float b = 1.0f; + private float a = 1.0f; + + /** Creates a new renderer with backing store of the specified width + and height. If alpha is true, allocates an alpha + channel in the backing store image. No mipmap support is + requested. + + @param width the width of the texture to render into + @param height the height of the texture to render into + @param alpha whether to allocate an alpha channel for the texture + */ + public TextureRenderer(final GL2 gl, final int width, final int height, final boolean alpha) { + this(gl, width, height, alpha, false); + } + + /** Creates a new renderer with backing store of the specified width + and height. If alpha is true, allocates an alpha channel in the + backing store image. If mipmap is true, attempts to use OpenGL's + automatic mipmap generation for better smoothing when rendering + the TextureRenderer's contents at a distance. + + @param width the width of the texture to render into + @param height the height of the texture to render into + @param alpha whether to allocate an alpha channel for the texture + @param mipmap whether to attempt use of automatic mipmap generation + */ + public TextureRenderer(final GL2 gl, final int width, final int height, final boolean alpha, final boolean mipmap) { + this(gl, width, height, alpha, false, mipmap); + } + + // Internal constructor to avoid confusion since alpha only makes + // sense when intensity is not set + private TextureRenderer(final GL2 gl, final int width, final int height, final boolean alpha, final boolean intensity, final boolean mipmap) { + this.gl = gl; + this.mipmap = mipmap; + init(width, height); + } + + /** Creates a new renderer with a special kind of backing store + which acts only as an alpha channel. No mipmap support is + requested. Internally, this associates a GL_INTENSITY OpenGL + texture with the backing store. */ + public static TextureRenderer createAlphaOnlyRenderer(final GL2 gl, final int width, final int height) { + return createAlphaOnlyRenderer(gl, width, height, false); + } + + /** Creates a new renderer with a special kind of backing store + which acts only as an alpha channel. If mipmap is + true, attempts to use OpenGL's automatic mipmap generation for + better smoothing when rendering the TextureRenderer's contents + at a distance. Internally, this associates a GL_INTENSITY OpenGL + texture with the backing store. */ + public static TextureRenderer createAlphaOnlyRenderer(final GL2 gl, final int width, final int height, final boolean mipmap) { + return new TextureRenderer(gl, width, height, false, true, mipmap); + } + + /** Returns the width of the backing store of this renderer. + + @return the width of the backing store of this renderer + */ + public int getWidth() { + return image.getWidth(); + } + + /** Returns the height of the backing store of this renderer. + + @return the height of the backing store of this renderer + */ + public int getHeight() { + return image.getHeight(); + } + + /** Returns the size of the backing store of this renderer in a + newly-allocated {@link Dimension Dimension} object. + + @return the size of the backing store of this renderer + */ + public Dimension getSize() { + return getSize(null); + } + + /** Returns the size of the backing store of this renderer. Uses the + {@link Dimension Dimension} object if one is supplied, + or allocates a new one if null is passed. + + @param d a {@link Dimension Dimension} object in which + to store the results, or null to allocate a new one + + @return the size of the backing store of this renderer + */ + public Dimension getSize(Dimension d) { + if (d == null) + d = new Dimension(); + d.setSize(image.getWidth(), image.getHeight()); + return d; + } + + /** Sets the size of the backing store of this renderer. This may + cause the OpenGL texture object associated with this renderer to + be invalidated; it is not recommended to cache this texture + object outside this class but to instead call {@link #getTexture + getTexture} when it is needed. + + @param width the new width of the backing store of this renderer + @param height the new height of the backing store of this renderer + + */ + public void setSize(final int width, final int height) { + init(width, height); + } + + /** Sets the size of the backing store of this renderer. This may + cause the OpenGL texture object associated with this renderer to + be invalidated. + + @param d the new size of the backing store of this renderer + + */ + public void setSize(final Dimension d) { + setSize(d.width, d.height); + } + + /** Sets whether smoothing is enabled for the OpenGL texture; if so, + uses GL_LINEAR interpolation for the minification and + magnification filters. Defaults to true. Changes to this setting + will not take effect until the next call to {@link + #beginRendering beginRendering}. + + @param smoothing whether smoothing is enabled for the OpenGL texture + */ + public void setSmoothing(final boolean smoothing) { + this.smoothing = smoothing; + smoothingChanged = true; + } + + /** Returns whether smoothing is enabled for the OpenGL texture; see + {@link #setSmoothing setSmoothing}. Defaults to true. + + @return whether smoothing is enabled for the OpenGL texture + */ + public boolean getSmoothing() { + return smoothing; + } + + /** Creates a {@link Graphics2D Graphics2D} instance for + rendering to the backing store of this renderer. The returned + object should be disposed of using the normal {@link + java.awt.Graphics#dispose() Graphics.dispose()} method once it + is no longer being used. + + @return a new {@link Graphics2D Graphics2D} object for + rendering into the backing store of this renderer + */ + public Graphics2D createGraphics() { + return image.createGraphics(); + } + + /** Returns the underlying Java 2D {@link Image Image} + being rendered into. */ + public Image getImage() { + return image; + } + + /** Marks the given region of the TextureRenderer as dirty. This + region, and any previously set dirty regions, will be + automatically synchronized with the underlying Texture during + the next {@link #getTexture getTexture} operation, at which + point the dirty region will be cleared. It is not necessary for + an OpenGL context to be current when this method is called. + + @param x the x coordinate (in Java 2D coordinates -- relative to + upper left) of the region to update + @param y the y coordinate (in Java 2D coordinates -- relative to + upper left) of the region to update + @param width the width of the region to update + @param height the height of the region to update + */ + public void markDirty(final int x, final int y, final int width, final int height) { + final Rectangle curRegion = new Rectangle(x, y, width, height); + if (dirtyRegion == null) { + dirtyRegion = curRegion; + } else { + dirtyRegion.add(curRegion); + } + } + + /** Returns the underlying OpenGL Texture object associated with + this renderer, synchronizing any dirty regions of the + TextureRenderer with the underlying OpenGL texture. + + + */ + public Texture getTexture() { + if (dirtyRegion != null) { + sync(dirtyRegion.x, dirtyRegion.y, dirtyRegion.width, dirtyRegion.height); + dirtyRegion = null; + } + + ensureTexture(); + return texture; + } + + /** Disposes all resources associated with this renderer. It is not + valid to use this renderer after calling this method. + + + */ + public void dispose() { + if (texture != null) { + texture.destroy(gl); + texture = null; + } + if (image != null) { + image.flush(); + image = null; + } + } + + /** Convenience method which assists in rendering portions of the + OpenGL texture to the screen as 2D quads in 3D space. Pushes + OpenGL state (GL_ENABLE_BIT); disables lighting; and enables the + texture in this renderer. Does not modify the depth test, back-face + culling, lighting, or the modelview or projection matrices. The + user is responsible for setting up the view matrices for correct + results of {@link #draw3DRect draw3DRect}. {@link + #end3DRendering} must be used in conjunction with this method to + restore all OpenGL states. + + + */ + public void begin3DRendering() { + beginRendering(); + } + + /** Changes the color of the polygons, and therefore the drawn + images, this TextureRenderer produces. Use of this method is + optional. The TextureRenderer uses the GL_MODULATE texture + environment mode, which causes the portions of the rendered + texture to be multiplied by the color of the rendered + polygons. The polygon color can be varied to achieve effects + like tinting of the overall output or fading in and out by + changing the alpha of the color.

+ + Each component ranges from 0.0f - 1.0f. The alpha component, if + used, does not need to be premultiplied into the color channels + as described in the documentation for {@link + com.jogamp.opengl.util.texture.Texture Texture}, although + premultiplied colors are used internally. The default color is + opaque white. + + @param r the red component of the new color + @param g the green component of the new color + @param b the blue component of the new color + @param a the alpha component of the new color, 0.0f = completely + transparent, 1.0f = completely opaque + + */ + public void setColor(final float r, final float g, final float b, final float a) { + this.r = r * a; + this.g = g * a; + this.b = b * a; + this.a = a; + + gl.glColor4f(this.r, this.g, this.b, this.a); + } + + private float[] compArray; + /** Changes the current color of this TextureRenderer to the + supplied one. The default color is opaque white. See {@link + #setColor(float,float,float,float) setColor} for more details. + + @param color the new color to use for rendering + + */ + public void setColor(final Color color) { + // Get color's RGBA components as floats in the range [0,1]. + if (compArray == null) { + compArray = new float[4]; + } + color.getRGBComponents(compArray); + setColor(compArray[0], compArray[1], compArray[2], compArray[3]); + } + + /** Draws an orthographically projected rectangle containing all of + the underlying texture to the specified location on the + screen. All (x, y) coordinates are specified relative to the + lower left corner of either the texture image or the current + OpenGL drawable. This method is equivalent to + drawOrthoRect(screenx, screeny, 0, 0, getWidth(), + getHeight());. + + @param screenx the on-screen x coordinate at which to draw the rectangle + @param screeny the on-screen y coordinate (relative to lower left) at + which to draw the rectangle + + + */ + public void drawOrthoRect(final int screenx, final int screeny) { + drawOrthoRect(screenx, screeny, 0, 0, getWidth(), getHeight()); + } + + /** Draws an orthographically projected rectangle of the underlying + texture to the specified location on the screen. All (x, y) + coordinates are specified relative to the lower left corner of + either the texture image or the current OpenGL drawable. + + @param screenx the on-screen x coordinate at which to draw the rectangle + @param screeny the on-screen y coordinate (relative to lower left) at + which to draw the rectangle + @param texturex the x coordinate of the pixel in the texture of + the lower left portion of the rectangle to draw + @param texturey the y coordinate of the pixel in the texture + (relative to lower left) of the lower left portion of the + rectangle to draw + @param width the width of the rectangle to draw + @param height the height of the rectangle to draw + + + */ + public void drawOrthoRect(final int screenx, final int screeny, + final int texturex, final int texturey, + final int width, final int height) { + draw3DRect(screenx, screeny, 0, texturex, texturey, width, height, 1); + } + + /** Draws a rectangle of the underlying texture to the specified 3D + location. In the current coordinate system, the lower left + corner of the rectangle is placed at (x, y, z), and the upper + right corner is placed at (x + width * scaleFactor, y + height * + scaleFactor, z). The lower left corner of the sub-rectangle of + the texture is (texturex, texturey) and the upper right corner + is (texturex + width, texturey + height). For back-face culling + purposes, the rectangle is drawn with counterclockwise + orientation of the vertices when viewed from the front. + + @param x the x coordinate at which to draw the rectangle + @param y the y coordinate at which to draw the rectangle + @param z the z coordinate at which to draw the rectangle + @param texturex the x coordinate of the pixel in the texture of + the lower left portion of the rectangle to draw + @param texturey the y coordinate of the pixel in the texture + (relative to lower left) of the lower left portion of the + rectangle to draw + @param width the width in texels of the rectangle to draw + @param height the height in texels of the rectangle to draw + @param scaleFactor the scale factor to apply (multiplicatively) + to the size of the drawn rectangle + + + */ + public void draw3DRect(final float x, final float y, final float z, + final int texturex, final int texturey, + final int width, final int height, + final float scaleFactor) { + // TODO: use VBA instead + final Texture texture = getTexture(); + final TextureCoords coords = texture.getSubImageTexCoords(texturex, texturey, + texturex + width, + texturey + height); + gl.glBegin(gl.GL_QUADS()); + gl.glTexCoord2f(coords.left(), coords.bottom()); + gl.glVertex3f(x, y, z); + gl.glTexCoord2f(coords.right(), coords.bottom()); + gl.glVertex3f(x + width * scaleFactor, y, z); + gl.glTexCoord2f(coords.right(), coords.top()); + gl.glVertex3f(x + width * scaleFactor, y + height * scaleFactor, z); + gl.glTexCoord2f(coords.left(), coords.top()); + gl.glVertex3f(x, y + height * scaleFactor, z); + gl.glEnd(); + } + + /** Convenience method which assists in rendering portions of the + OpenGL texture to the screen as 2D quads in 3D space. Must be + used if {@link #begin3DRendering} is used to set up the + rendering stage for this overlay. + + + */ + public void end3DRendering() { + endRendering(); + } + + /** Indicates whether automatic mipmap generation is in use for this + TextureRenderer. The result of this method may change from true + to false if it is discovered during allocation of the + TextureRenderer's backing store that automatic mipmap generation + is not supported at the OpenGL level. */ + public boolean isUsingAutoMipmapGeneration() { + return mipmap; + } + + //---------------------------------------------------------------------- + // Internals only below this point + // + + private void beginRendering() { + final int attribBits = gl.GL_ENABLE_BIT() | gl.GL_TEXTURE_BIT() | gl.GL_COLOR_BUFFER_BIT(); + gl.glPushAttrib(attribBits); + gl.glDisable(gl.GL_LIGHTING()); + gl.glEnable(gl.GL_BLEND()); + gl.glBlendFunc(gl.GL_ONE(), gl.GL_ONE_MINUS_SRC_ALPHA()); + final Texture texture = getTexture(); + texture.enable(gl); + texture.bind(gl); + gl.glTexEnvi(gl.GL_TEXTURE_ENV(), gl.GL_TEXTURE_ENV_MODE(), gl.GL_MODULATE()); + // Change polygon color to last saved + gl.glColor4f(r, g, b, a); + if (smoothingChanged) { + smoothingChanged = false; + if (smoothing) { + texture.setTexParameteri(gl, gl.GL_TEXTURE_MAG_FILTER(), gl.GL_LINEAR()); + if (mipmap) { + texture.setTexParameteri(gl, gl.GL_TEXTURE_MIN_FILTER(), gl.GL_LINEAR_MIPMAP_LINEAR()); + } else { + texture.setTexParameteri(gl, gl.GL_TEXTURE_MIN_FILTER(), gl.GL_LINEAR()); + } + } else { + texture.setTexParameteri(gl, gl.GL_TEXTURE_MIN_FILTER(), gl.GL_NEAREST()); + texture.setTexParameteri(gl, gl.GL_TEXTURE_MAG_FILTER(), gl.GL_NEAREST()); + } + } + } + + private void endRendering() { + final Texture texture = getTexture(); + texture.disable(gl); + gl.glPopAttrib(); + } + + private void init(final int width, final int height) { + // Discard previous BufferedImage if any + if (image != null) { + image.flush(); + image = null; + } + + // Infer the internal format if not an intensity texture + final int internalFormat = gl.GL_INTENSITY(); + final int imageType = BufferedImage.TYPE_BYTE_GRAY; + image = new BufferedImage(width, height, imageType); + // Always realllocate the TextureData associated with this + // BufferedImage; it's just a reference to the contents but we + // need it in order to update sub-regions of the underlying + // texture + textureData = new AWTTextureData(gl, internalFormat, mipmap, image); + // For now, always reallocate the underlying OpenGL texture when + // the backing store size changes + mustReallocateTexture = true; + } + + /** Synchronizes the specified region of the backing store down to + the underlying OpenGL texture. If {@link #markDirty markDirty} + is used instead to indicate the regions that are out of sync, + this method does not need to be called. + + @param x the x coordinate (in Java 2D coordinates -- relative to + upper left) of the region to update + @param y the y coordinate (in Java 2D coordinates -- relative to + upper left) of the region to update + @param width the width of the region to update + @param height the height of the region to update + + + */ + private void sync(final int x, final int y, final int width, final int height) { + // Force allocation if necessary + final boolean canSkipUpdate = ensureTexture(); + + if (!canSkipUpdate) { + // Update specified region. + // NOTE that because BufferedImage-based TextureDatas now don't + // do anything to their contents, the coordinate systems for + // OpenGL and Java 2D actually line up correctly for + // updateSubImage calls, so we don't need to do any argument + // conversion here (i.e., flipping the Y coordinate). + texture.updateSubImage(gl, textureData, 0, x, y, x, y, width, height); + } + } + + // Returns true if the texture was newly allocated, false if not + private boolean ensureTexture() { + if (mustReallocateTexture) { + if (texture != null) { + texture.destroy(gl); + texture = null; + } + mustReallocateTexture = false; + } + + if (texture == null) { + texture = new Texture(gl, textureData); + if (mipmap && !texture.isUsingAutoMipmapGeneration()) { + // Only try this once + texture.destroy(gl); + mipmap = false; + textureData.setMipmap(false); + texture = new Texture(gl, textureData); + } + + if (!smoothing) { + // The TextureIO classes default to GL_LINEAR filtering + texture.setTexParameteri(gl, gl.GL_TEXTURE_MIN_FILTER(), gl.GL_NEAREST()); + texture.setTexParameteri(gl, gl.GL_TEXTURE_MAG_FILTER(), gl.GL_NEAREST()); + } + return true; + } + + return false; + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderCode.java b/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderCode.java new file mode 100644 index 00000000..66f000b1 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderCode.java @@ -0,0 +1,1448 @@ +/** + * Copyright 2010 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ + +package net.opengrabeso.opengl.util.glsl; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.StringReader; +import java.net.URISyntaxException; +import java.net.URLConnection; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Set; + +import com.jogamp.opengl.*; + + +import jogamp.opengl.Debug; + +import com.jogamp.common.net.Uri; +import com.jogamp.common.nio.Buffers; +import com.jogamp.common.util.IOUtil; +import com.jogamp.common.util.VersionNumber; + +/** + * Convenient shader code class to use and instantiate vertex or fragment programs. + *

+ * A documented example of how to use this code is available + * {@link #create(GL2ES2, int, Class, String, String, String, boolean) here} and + * {@link #create(GL2ES2, int, int, Class, String, String[], String, String) here}. + *

+ *

+ * Support for {@link GL4#GL_TESS_CONTROL_SHADER} and {@link GL4#GL_TESS_EVALUATION_SHADER} + * was added since 2.2.1. + *

+ */ +public class ShaderCode { + public static final boolean DEBUG_CODE = Debug.isPropertyDefined("jogl.debug.GLSLCode", true); + + /** Unique resource suffix for {@link GL2ES2#GL_VERTEX_SHADER} in source code: {@value} */ + public static final String SUFFIX_VERTEX_SOURCE = "vp" ; + + /** Unique resource suffix for {@link GL2ES2#GL_VERTEX_SHADER} in binary: {@value} */ + public static final String SUFFIX_VERTEX_BINARY = "bvp" ; + + /** Unique resource suffix for {@link GL3#GL_GEOMETRY_SHADER} in source code: {@value} */ + public static final String SUFFIX_GEOMETRY_SOURCE = "gp" ; + + /** Unique resource suffix for {@link GL3#GL_GEOMETRY_SHADER} in binary: {@value} */ + public static final String SUFFIX_GEOMETRY_BINARY = "bgp" ; + + /** + * Unique resource suffix for {@link GL3ES3#GL_COMPUTE_SHADER} in source code: {@value} + * @since 2.3.2 + */ + public static final String SUFFIX_COMPUTE_SOURCE = "cp" ; + + /** + * Unique resource suffix for {@link GL3ES3#GL_COMPUTE_SHADER} in binary: {@value} + * @since 2.3.2 + */ + public static final String SUFFIX_COMPUTE_BINARY = "bcp" ; + + /** + * Unique resource suffix for {@link GL4#GL_TESS_CONTROL_SHADER} in source code: {@value} + * @since 2.2.1 + */ + public static final String SUFFIX_TESS_CONTROL_SOURCE = "tcp" ; + + /** + * Unique resource suffix for {@link GL4#GL_TESS_CONTROL_SHADER} in binary: {@value} + * @since 2.2.1 + */ + public static final String SUFFIX_TESS_CONTROL_BINARY = "btcp" ; + + /** + * Unique resource suffix for {@link GL4#GL_TESS_EVALUATION_SHADER} in source code: {@value} + * @since 2.2.1 + */ + public static final String SUFFIX_TESS_EVALUATION_SOURCE = "tep" ; + + /** + * Unique resource suffix for {@link GL4#GL_TESS_EVALUATION_SHADER} in binary: {@value} + * @since 2.2.1 + */ + public static final String SUFFIX_TESS_EVALUATION_BINARY = "btep" ; + + /** Unique resource suffix for {@link GL2ES2#GL_FRAGMENT_SHADER} in source code: {@value} */ + public static final String SUFFIX_FRAGMENT_SOURCE = "fp" ; + + /** Unique resource suffix for {@link GL2ES2#GL_FRAGMENT_SHADER} in binary: {@value} */ + public static final String SUFFIX_FRAGMENT_BINARY = "bfp" ; + + /** Unique relative path for binary shader resources for {@link GLES2#GL_NVIDIA_PLATFORM_BINARY_NV NVIDIA}: {@value} */ + public static final String SUB_PATH_NVIDIA = "nvidia" ; + + /** + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param count number of shaders + * @param source CharSequence array containing the shader sources, organized as source[count][strings-per-shader]. + * May be either an immutable String - or mutable StringBuilder array. + * + * @throws IllegalArgumentException if count and source.length do not match + */ + public ShaderCode(final int type, final int count, final CharSequence[][] source) { + if(source.length != count) { + throw new IllegalArgumentException("shader number ("+count+") and sourceFiles array ("+source.length+") of different lenght."); + } + switch (type) { + case GL2ES2.GL_VERTEX_SHADER: + case GL2ES2.GL_FRAGMENT_SHADER: + case GL3ES3.GL_GEOMETRY_SHADER: + case GL3ES3.GL_TESS_CONTROL_SHADER: + case GL3ES3.GL_TESS_EVALUATION_SHADER: + case GL3ES3.GL_COMPUTE_SHADER: + break; + default: + throw new GLException("Unknown shader type: "+type); + } + shaderSource = source; + shaderBinaryFormat = -1; + shaderBinary = null; + shaderType = type; + shader = Buffers.newDirectIntBuffer(count); + id = getNextID(); + + if(DEBUG_CODE) { + System.out.println("Created: "+toString()); + // dumpShaderSource(System.out); // already done in readShaderSource + } + } + + /** + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param count number of shaders + * @param binary binary buffer containing the shader binaries, + */ + public ShaderCode(final int type, final int count, final int binFormat, final Buffer binary) { + switch (type) { + case GL2ES2.GL_VERTEX_SHADER: + case GL2ES2.GL_FRAGMENT_SHADER: + case GL3ES3.GL_GEOMETRY_SHADER: + case GL3ES3.GL_TESS_CONTROL_SHADER: + case GL3ES3.GL_TESS_EVALUATION_SHADER: + case GL3ES3.GL_COMPUTE_SHADER: + break; + default: + throw new GLException("Unknown shader type: "+type); + } + shaderSource = null; + shaderBinaryFormat = binFormat; + shaderBinary = binary; + shaderType = type; + shader = Buffers.newDirectIntBuffer(count); + id = getNextID(); + } + + /** + * Creates a complete {@link ShaderCode} object while reading all shader source of sourceFiles, + * which location is resolved using the context class, see {@link #readShaderSource(Class, String)}. + * + * @param gl current GL object to determine whether a shader compiler is available. If null, no validation is performed. + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param count number of shaders + * @param context class used to help resolving the source location + * @param sourceFiles array of source locations, organized as sourceFiles[count] -> shaderSources[count][1] + * @param mutableStringBuilder if true method returns a mutable StringBuilder instance + * which can be edited later on at the costs of a String conversion when passing to + * {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)}. + * If false method returns an immutable String instance, + * which can be passed to {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)} + * at no additional costs. + * + * @throws IllegalArgumentException if count and sourceFiles.length do not match + * @see #readShaderSource(Class, String) + */ + public static ShaderCode create(final GL2ES2 gl, final int type, final int count, final Class context, + final String[] sourceFiles, final boolean mutableStringBuilder) { + if(null != gl && !ShaderUtil.isShaderCompilerAvailable(gl)) { + return null; + } + + CharSequence[][] shaderSources = null; + if(null!=sourceFiles) { + // sourceFiles.length and count is validated in ctor + shaderSources = new CharSequence[sourceFiles.length][1]; + for(int i=0; isourceLocations + * via {@link #readShaderSource(Uri, boolean)}. + * + * @param gl current GL object to determine whether a shader compiler is available. If null, no validation is performed. + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param count number of shaders + * @param sourceLocations array of {@link Uri} source locations, organized as sourceFiles[count] -> shaderSources[count][1] + * @param mutableStringBuilder if true method returns a mutable StringBuilder instance + * which can be edited later on at the costs of a String conversion when passing to + * {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)}. + * If false method returns an immutable String instance, + * which can be passed to {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)} + * at no additional costs. + * + * @throws IllegalArgumentException if count and sourceFiles.length do not match + * @see #readShaderSource(Uri, boolean) + * @since 2.3.2 + */ + public static ShaderCode create(final GL2ES2 gl, final int type, final int count, + final Uri[] sourceLocations, final boolean mutableStringBuilder) { + if(null != gl && !ShaderUtil.isShaderCompilerAvailable(gl)) { + return null; + } + + CharSequence[][] shaderSources = null; + if(null!=sourceLocations) { + // sourceFiles.length and count is validated in ctor + shaderSources = new CharSequence[sourceLocations.length][1]; + for(int i=0; ibinaryFile, + * which location is resolved using the context class, see {@link #readShaderBinary(Class, String)}. + * + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param count number of shaders + * @param context class used to help resolving the source location + * @param binFormat a valid native binary format as they can be queried by {@link ShaderUtil#getShaderBinaryFormats(GL)}. + * @param sourceFiles array of source locations, organized as sourceFiles[count] + * + * @see #readShaderBinary(Class, String) + * @see ShaderUtil#getShaderBinaryFormats(GL) + */ + public static ShaderCode create(final int type, final int count, final Class context, int binFormat, final String binaryFile) { + ByteBuffer shaderBinary = null; + if(null!=binaryFile && 0<=binFormat) { + try { + shaderBinary = readShaderBinary(context, binaryFile); + } catch (final IOException ioe) { + throw new RuntimeException("readShaderBinary("+binaryFile+") error: ", ioe); + } + if(null == shaderBinary) { + binFormat = -1; + } + } + if(null==shaderBinary) { + return null; + } + return new ShaderCode(type, count, binFormat, shaderBinary); + } + + /** + * Creates a complete {@link ShaderCode} object while reading the shader binary from {@link Uri} binLocations + * via {@link #readShaderBinary(Uri)}. + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param count number of shaders + * @param binFormat a valid native binary format as they can be queried by {@link ShaderUtil#getShaderBinaryFormats(GL)}. + * @param binLocations {@link Uri} binary location + * + * @see #readShaderBinary(Uri) + * @see ShaderUtil#getShaderBinaryFormats(GL) + * @since 2.3.2 + */ + public static ShaderCode create(final int type, final int count, int binFormat, final Uri binLocation) { + ByteBuffer shaderBinary = null; + if(null!=binLocation && 0<=binFormat) { + try { + shaderBinary = readShaderBinary(binLocation); + } catch (final IOException ioe) { + throw new RuntimeException("readShaderBinary("+binLocation+") error: ", ioe); + } + if(null == shaderBinary) { + binFormat = -1; + } + } + if(null==shaderBinary) { + return null; + } + return new ShaderCode(type, count, binFormat, shaderBinary); + } + + /** + * Returns a unique suffix for shader resources as follows: + *
    + *
  • Source
      + *
    • {@link GL2ES2#GL_VERTEX_SHADER vertex}: {@link #SUFFIX_VERTEX_SOURCE}
    • + *
    • {@link GL2ES2#GL_FRAGMENT_SHADER fragment}: {@link #SUFFIX_FRAGMENT_SOURCE}
    • + *
    • {@link GL3#GL_GEOMETRY_SHADER geometry}: {@link #SUFFIX_GEOMETRY_SOURCE}
    • + *
    • {@link GL4#GL_TESS_CONTROL_SHADER tess-ctrl}: {@link #SUFFIX_TESS_CONTROL_SOURCE}
    • + *
    • {@link GL4#GL_TESS_EVALUATION_SHADER tess-eval}: {@link #SUFFIX_TESS_EVALUATION_SOURCE}
    • + *
    • {@link GL3ES3#GL_COMPUTE_SHADER}: {@link #SUFFIX_COMPUTE_SOURCE}
    • + *
  • + *
  • Binary
      + *
    • {@link GL2ES2#GL_VERTEX_SHADER vertex}: {@link #SUFFIX_VERTEX_BINARY}
    • + *
    • {@link GL2ES2#GL_FRAGMENT_SHADER fragment}: {@link #SUFFIX_FRAGMENT_BINARY}
    • + *
    • {@link GL3#GL_GEOMETRY_SHADER geometry}: {@link #SUFFIX_GEOMETRY_BINARY}
    • + *
    • {@link GL4#GL_TESS_CONTROL_SHADER tess-ctrl}: {@link #SUFFIX_TESS_CONTROL_BINARY}
    • + *
    • {@link GL4#GL_TESS_EVALUATION_SHADER tess-eval}: {@link #SUFFIX_TESS_EVALUATION_BINARY}
    • + *
    • {@link GL3ES3#GL_COMPUTE_SHADER}: {@link #SUFFIX_COMPUTE_BINARY}
    • + *
  • + *
+ * @param binary true for a binary resource, false for a source resource + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * + + * + * @see #create(GL2ES2, int, Class, String, String, String, boolean) + */ + public static String getFileSuffix(final boolean binary, final int type) { + switch (type) { + case GL2ES2.GL_VERTEX_SHADER: + return binary?SUFFIX_VERTEX_BINARY:SUFFIX_VERTEX_SOURCE; + case GL2ES2.GL_FRAGMENT_SHADER: + return binary?SUFFIX_FRAGMENT_BINARY:SUFFIX_FRAGMENT_SOURCE; + case GL3ES3.GL_GEOMETRY_SHADER: + return binary?SUFFIX_GEOMETRY_BINARY:SUFFIX_GEOMETRY_SOURCE; + case GL3ES3.GL_TESS_CONTROL_SHADER: + return binary?SUFFIX_TESS_CONTROL_BINARY:SUFFIX_TESS_CONTROL_SOURCE; + case GL3ES3.GL_TESS_EVALUATION_SHADER: + return binary?SUFFIX_TESS_EVALUATION_BINARY:SUFFIX_TESS_EVALUATION_SOURCE; + case GL3ES3.GL_COMPUTE_SHADER: + return binary?SUFFIX_COMPUTE_BINARY:SUFFIX_COMPUTE_SOURCE; + default: + throw new GLException("illegal shader type: "+type); + } + } + + /** + * Returns a unique relative path for binary shader resources as follows: + *
    + *
  • {@link GLES2#GL_NVIDIA_PLATFORM_BINARY_NV NVIDIA}: {@link #SUB_PATH_NVIDIA}
  • + *
+ * + + * + * @see #create(GL2ES2, int, Class, String, String, String, boolean) + */ + public static String getBinarySubPath(final int binFormat) { + switch (binFormat) { + case GLES2.GL_NVIDIA_PLATFORM_BINARY_NV: + return SUB_PATH_NVIDIA; + default: + throw new GLException("unsupported binary format: "+binFormat); + } + } + + /** + * Convenient creation method for instantiating a complete {@link ShaderCode} object + * either from source code using {@link #create(GL2ES2, int, int, Class, String[])}, + * or from a binary code using {@link #create(int, int, Class, int, String)}, + * whatever is available first. + *

+ * The source and binary location names are expected w/o suffixes which are + * resolved and appended using the given {@code srcSuffixOpt} and {@code binSuffixOpt} + * if not {@code null}, otherwise {@link #getFileSuffix(boolean, int)} determines the suffixes. + *

+ *

+ * Additionally, the binary resource is expected within a subfolder of binRoot + * which reflects the vendor specific binary format, see {@link #getBinarySubPath(int)}. + * All {@link ShaderUtil#getShaderBinaryFormats(GL)} are being iterated + * using the binary subfolder, the first existing resource is being used. + *

+ * + * Example: + *
+     *   Your std JVM layout (plain or within a JAR):
+     *
+     *      org/test/glsl/MyShaderTest.class
+     *      org/test/glsl/shader/vertex.vp
+     *      org/test/glsl/shader/fragment.fp
+     *      org/test/glsl/shader/bin/nvidia/vertex.bvp
+     *      org/test/glsl/shader/bin/nvidia/fragment.bfp
+     *
+     *   Your Android APK layout:
+     *
+     *      classes.dex
+     *      assets/org/test/glsl/shader/vertex.vp
+     *      assets/org/test/glsl/shader/fragment.fp
+     *      assets/org/test/glsl/shader/bin/nvidia/vertex.bvp
+     *      assets/org/test/glsl/shader/bin/nvidia/fragment.bfp
+     *      ...
+     *
+     *   Your invocation in org/test/glsl/MyShaderTest.java:
+     *
+     *      ShaderCode vp0 = ShaderCode.create(gl, GL2ES2.GL_VERTEX_SHADER, 1, this.getClass(),
+     *                                         "shader", new String[] { "vertex" }, null,
+     *                                         "shader/bin", "vertex", null, true);
+     *      ShaderCode fp0 = ShaderCode.create(gl, GL2ES2.GL_FRAGMENT_SHADER, 1, this.getClass(),
+     *                                         "shader", new String[] { "vertex" }, null,
+     *                                         "shader/bin", "fragment", null, true);
+     *      ShaderProgram sp0 = new ShaderProgram();
+     *      sp0.add(gl, vp0, System.err);
+     *      sp0.add(gl, fp0, System.err);
+     *      st.attachShaderProgram(gl, sp0, true);
+     * 
+ * A simplified entry point is {@link #create(GL2ES2, int, Class, String, String, String, boolean)}. + * + *

+ * The location is finally being resolved using the context class, see {@link #readShaderBinary(Class, String)}. + *

+ * + * @param gl current GL object to determine whether a shader compiler is available (if source is used), + * or to determine the shader binary format (if binary is used). + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param count number of shaders + * @param context class used to help resolving the source and binary location + * @param srcRoot relative root path for srcBasenames optional + * @param srcBasenames basenames w/o path or suffix relative to srcRoot for the shader's source code + * @param srcSuffixOpt optional custom suffix for shader's source file, + * if {@code null} {@link #getFileSuffix(boolean, int)} is being used. + * @param binRoot relative root path for binBasenames + * @param binBasename basename w/o path or suffix relative to binRoot for the shader's binary code + * @param binSuffixOpt optional custom suffix for shader's binary file, + * if {@code null} {@link #getFileSuffix(boolean, int)} is being used. + * @param mutableStringBuilder if true method returns a mutable StringBuilder instance + * which can be edited later on at the costs of a String conversion when passing to + * {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)}. + * If false method returns an immutable String instance, + * which can be passed to {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)} + * at no additional costs. + * + * @throws IllegalArgumentException if count and srcBasenames.length do not match + * + * @see #create(GL2ES2, int, int, Class, String[]) + * @see #create(int, int, Class, int, String) + * @see #readShaderSource(Class, String) + * @see #getFileSuffix(boolean, int) + * @see ShaderUtil#getShaderBinaryFormats(GL) + * @see #getBinarySubPath(int) + * + * @since 2.3.2 + */ + public static ShaderCode create(final GL2ES2 gl, final int type, final int count, final Class context, + final String srcRoot, final String[] srcBasenames, final String srcSuffixOpt, + final String binRoot, final String binBasename, final String binSuffixOpt, + final boolean mutableStringBuilder) { + ShaderCode res = null; + final String srcPath[]; + String srcPathString = null; + String binFileName = null; + + if( null!=srcBasenames && ShaderUtil.isShaderCompilerAvailable(gl) ) { + srcPath = new String[srcBasenames.length]; + final String srcSuffix = null != srcSuffixOpt ? srcSuffixOpt : getFileSuffix(false, type); + if( null != srcRoot && srcRoot.length() > 0 ) { + for(int i=0; i binFmts = ShaderUtil.getShaderBinaryFormats(gl); + final String binSuffix = null != binSuffixOpt ? binSuffixOpt : getFileSuffix(true, type); + for(final Iterator iter=binFmts.iterator(); iter.hasNext(); ) { + final int bFmt = iter.next().intValue(); + final String bFmtPath = getBinarySubPath(bFmt); + if(null==bFmtPath) continue; + binFileName = binRoot + '/' + bFmtPath + '/' + binBasename + "." + binSuffix; + res = create(type, count, context, bFmt, binFileName); + if(null!=res) { + return res; + } + } + } + throw new GLException("No shader code found (source nor binary) for src: "+srcPathString+ + ", bin: "+binFileName); + } + + /** + * Simplified variation of {@link #create(GL2ES2, int, int, Class, String, String[], String, String, String, String, boolean)}. + *

+ * Convenient creation method for instantiating a complete {@link ShaderCode} object + * either from source code using {@link #create(GL2ES2, int, int, Class, String[])}, + * or from a binary code using {@link #create(int, int, Class, int, String)}, + * whatever is available first. + *

+ *

+ * The source and binary location names are expected w/o suffixes which are + * resolved and appended using {@link #getFileSuffix(boolean, int)}. + *

+ *

+ * Additionally, the binary resource is expected within a subfolder of binRoot + * which reflects the vendor specific binary format, see {@link #getBinarySubPath(int)}. + * All {@link ShaderUtil#getShaderBinaryFormats(GL)} are being iterated + * using the binary subfolder, the first existing resource is being used. + *

+ * + * Example: + *
+     *   Your std JVM layout (plain or within a JAR):
+     *
+     *      org/test/glsl/MyShaderTest.class
+     *      org/test/glsl/shader/vertex.vp
+     *      org/test/glsl/shader/fragment.fp
+     *      org/test/glsl/shader/bin/nvidia/vertex.bvp
+     *      org/test/glsl/shader/bin/nvidia/fragment.bfp
+     *
+     *   Your Android APK layout:
+     *
+     *      classes.dex
+     *      assets/org/test/glsl/shader/vertex.vp
+     *      assets/org/test/glsl/shader/fragment.fp
+     *      assets/org/test/glsl/shader/bin/nvidia/vertex.bvp
+     *      assets/org/test/glsl/shader/bin/nvidia/fragment.bfp
+     *      ...
+     *
+     *   Your invocation in org/test/glsl/MyShaderTest.java:
+     *
+     *      ShaderCode vp0 = ShaderCode.create(gl, GL2ES2.GL_VERTEX_SHADER, 1, this.getClass(),
+     *                                         "shader", new String[] { "vertex" }, "shader/bin", "vertex", true);
+     *      ShaderCode fp0 = ShaderCode.create(gl, GL2ES2.GL_FRAGMENT_SHADER, 1, this.getClass(),
+     *                                         "shader", new String[] { "vertex" }, "shader/bin", "fragment", true);
+     *      ShaderProgram sp0 = new ShaderProgram();
+     *      sp0.add(gl, vp0, System.err);
+     *      sp0.add(gl, fp0, System.err);
+     *      st.attachShaderProgram(gl, sp0, true);
+     * 
+ * A simplified entry point is {@link #create(GL2ES2, int, Class, String, String, String, boolean)}. + * + *

+ * The location is finally being resolved using the context class, see {@link #readShaderBinary(Class, String)}. + *

+ * + * @param gl current GL object to determine whether a shader compiler is available (if source is used), + * or to determine the shader binary format (if binary is used). + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param count number of shaders + * @param context class used to help resolving the source and binary location + * @param srcRoot relative root path for srcBasenames optional + * @param srcBasenames basenames w/o path or suffix relative to srcRoot for the shader's source code + * @param binRoot relative root path for binBasenames + * @param binBasename basename w/o path or suffix relative to binRoot for the shader's binary code + * @param mutableStringBuilder if true method returns a mutable StringBuilder instance + * which can be edited later on at the costs of a String conversion when passing to + * {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)}. + * If false method returns an immutable String instance, + * which can be passed to {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)} + * at no additional costs. + * + * @throws IllegalArgumentException if count and srcBasenames.length do not match + * + * @see #create(GL2ES2, int, int, Class, String, String[], String, String, String, String, boolean) + * @see #readShaderSource(Class, String) + * @see #getFileSuffix(boolean, int) + * @see ShaderUtil#getShaderBinaryFormats(GL) + * @see #getBinarySubPath(int) + */ + public static ShaderCode create(final GL2ES2 gl, final int type, final int count, final Class context, + final String srcRoot, final String[] srcBasenames, + final String binRoot, final String binBasename, + final boolean mutableStringBuilder) { + return create(gl, type, count, context, srcRoot, srcBasenames, null, + binRoot, binBasename, null, mutableStringBuilder); + } + + /** + * Simplified variation of {@link #create(GL2ES2, int, int, Class, String, String[], String, String, String, String, boolean)}. + *

+ * Example: + *

+     *   Your std JVM layout (plain or within a JAR):
+     *
+     *      org/test/glsl/MyShaderTest.class
+     *      org/test/glsl/shader/vertex.vp
+     *      org/test/glsl/shader/fragment.fp
+     *      org/test/glsl/shader/bin/nvidia/vertex.bvp
+     *      org/test/glsl/shader/bin/nvidia/fragment.bfp
+     *
+     *   Your Android APK layout:
+     *
+     *      classes.dex
+     *      assets/org/test/glsl/shader/vertex.vp
+     *      assets/org/test/glsl/shader/fragment.fp
+     *      assets/org/test/glsl/shader/bin/nvidia/vertex.bvp
+     *      assets/org/test/glsl/shader/bin/nvidia/fragment.bfp
+     *      ...
+     *
+     *   Your invocation in org/test/glsl/MyShaderTest.java:
+     *
+     *      ShaderCode vp0 = ShaderCode.create(gl, GL2ES2.GL_VERTEX_SHADER, this.getClass(),
+     *                                         "shader", "shader/bin", "vertex", null, null, true);
+     *      ShaderCode fp0 = ShaderCode.create(gl, GL2ES2.GL_FRAGMENT_SHADER, this.getClass(),
+     *                                         "shader", "shader/bin", "fragment", null, null, true);
+     *      ShaderProgram sp0 = new ShaderProgram();
+     *      sp0.add(gl, vp0, System.err);
+     *      sp0.add(gl, fp0, System.err);
+     *      st.attachShaderProgram(gl, sp0, true);
+     * 
+ *

+ * + * @param gl current GL object to determine whether a shader compiler is available (if source is used), + * or to determine the shader binary format (if binary is used). + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param context class used to help resolving the source and binary location + * @param srcRoot relative root path for basename optional + * @param binRoot relative root path for basename + * @param basename basename w/o path or suffix relative to srcRoot and binRoot + * for the shader's source and binary code. + * @param srcSuffixOpt optional custom suffix for shader's source file, + * if {@code null} {@link #getFileSuffix(boolean, int)} is being used. + * @param binSuffixOpt optional custom suffix for shader's binary file, + * if {@code null} {@link #getFileSuffix(boolean, int)} is being used. + * @param mutableStringBuilder if true method returns a mutable StringBuilder instance + * which can be edited later on at the costs of a String conversion when passing to + * {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)}. + * If false method returns an immutable String instance, + * which can be passed to {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)} + * at no additional costs. + * @throws IllegalArgumentException if count is not 1 + * + * @see #create(GL2ES2, int, int, Class, String, String[], String, String, String, String, boolean) + * @since 2.3.2 + */ + public static ShaderCode create(final GL2ES2 gl, final int type, final Class context, + final String srcRoot, final String binRoot, + final String basename, final String srcSuffixOpt, final String binSuffixOpt, + final boolean mutableStringBuilder) { + return create(gl, type, 1, context, srcRoot, new String[] { basename }, srcSuffixOpt, + binRoot, basename, binSuffixOpt, mutableStringBuilder ); + } + + /** + * Simplified variation of {@link #create(GL2ES2, int, Class, String, String, String, String, String, boolean)}. + *

+ * Example: + *

+     *   Your std JVM layout (plain or within a JAR):
+     *
+     *      org/test/glsl/MyShaderTest.class
+     *      org/test/glsl/shader/vertex.vp
+     *      org/test/glsl/shader/fragment.fp
+     *      org/test/glsl/shader/bin/nvidia/vertex.bvp
+     *      org/test/glsl/shader/bin/nvidia/fragment.bfp
+     *
+     *   Your Android APK layout:
+     *
+     *      classes.dex
+     *      assets/org/test/glsl/shader/vertex.vp
+     *      assets/org/test/glsl/shader/fragment.fp
+     *      assets/org/test/glsl/shader/bin/nvidia/vertex.bvp
+     *      assets/org/test/glsl/shader/bin/nvidia/fragment.bfp
+     *      ...
+     *
+     *   Your invocation in org/test/glsl/MyShaderTest.java:
+     *
+     *      ShaderCode vp0 = ShaderCode.create(gl, GL2ES2.GL_VERTEX_SHADER, this.getClass(),
+     *                                         "shader", "shader/bin", "vertex", true);
+     *      ShaderCode fp0 = ShaderCode.create(gl, GL2ES2.GL_FRAGMENT_SHADER, this.getClass(),
+     *                                         "shader", "shader/bin", "fragment", true);
+     *      ShaderProgram sp0 = new ShaderProgram();
+     *      sp0.add(gl, vp0, System.err);
+     *      sp0.add(gl, fp0, System.err);
+     *      st.attachShaderProgram(gl, sp0, true);
+     * 
+ *

+ * + * @param gl current GL object to determine whether a shader compiler is available (if source is used), + * or to determine the shader binary format (if binary is used). + * @param type either {@link GL2ES2#GL_VERTEX_SHADER}, {@link GL2ES2#GL_FRAGMENT_SHADER}, {@link GL3#GL_GEOMETRY_SHADER}, + * {@link GL4#GL_TESS_CONTROL_SHADER}, {@link GL4#GL_TESS_EVALUATION_SHADER} or {@link GL3ES3#GL_COMPUTE_SHADER}. + * @param context class used to help resolving the source and binary location + * @param srcRoot relative root path for basename optional + * @param binRoot relative root path for basename + * @param mutableStringBuilder if true method returns a mutable StringBuilder instance + * which can be edited later on at the costs of a String conversion when passing to + * {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)}. + * If false method returns an immutable String instance, + * which can be passed to {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)} + * at no additional costs. + * @param basenames basename w/o path or suffix relative to srcRoot and binRoot + * for the shader's source and binary code. + * @param mutableStringBuilder if true method returns a mutable StringBuilder instance + * which can be edited later on at the costs of a String conversion when passing to + * {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)}. + * If false method returns an immutable String instance, + * which can be passed to {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)} + * at no additional costs. + * @throws IllegalArgumentException if count is not 1 + */ + public static ShaderCode create(final GL2ES2 gl, final int type, final Class context, + final String srcRoot, final String binRoot, final String basename, + final boolean mutableStringBuilder) { + return ShaderCode.create(gl, type, context, srcRoot, binRoot, basename, null, null, mutableStringBuilder); + } + + /** + * returns the uniq shader id as an integer + */ + public int id() { return id; } + + public int shaderType() { return shaderType; } + public String shaderTypeStr() { return shaderTypeStr(shaderType); } + + public static String shaderTypeStr(final int type) { + switch (type) { + case GL2ES2.GL_VERTEX_SHADER: + return "VERTEX_SHADER"; + case GL2ES2.GL_FRAGMENT_SHADER: + return "FRAGMENT_SHADER"; + case GL3ES3.GL_GEOMETRY_SHADER: + return "GEOMETRY_SHADER"; + case GL3ES3.GL_TESS_CONTROL_SHADER: + return "TESS_CONTROL_SHADER"; + case GL3ES3.GL_TESS_EVALUATION_SHADER: + return "TESS_EVALUATION_SHADER"; + case GL3ES3.GL_COMPUTE_SHADER: + return "COMPUTE_SHADER"; + } + return "UNKNOWN_SHADER"; + } + + public int shaderBinaryFormat() { return shaderBinaryFormat; } + public Buffer shaderBinary() { return shaderBinary; } + public CharSequence[][] shaderSource() { return shaderSource; } + + public boolean isValid() { return valid; } + + public IntBuffer shader() { return shader; } + + public boolean compile(final GL2ES2 gl) { + return compile(gl, null); + } + public boolean compile(final GL2ES2 gl, final PrintStream verboseOut) { + if(isValid()) return true; + + // Create & Compile the vertex/fragment shader objects + if(null!=shaderSource) { + if(DEBUG_CODE) { + System.err.println("ShaderCode.compile:"); + dumpShaderSource(System.err); + } + valid=ShaderUtil.createAndCompileShader(gl, shader, shaderType, + shaderSource, verboseOut); + } else if(null!=shaderBinary) { + valid=ShaderUtil.createAndLoadShader(gl, shader, shaderType, + shaderBinaryFormat, shaderBinary, verboseOut); + } else { + throw new GLException("no code (source or binary)"); + } + return valid; + } + + public void destroy(final GL2ES2 gl) { + if(isValid()) { + if(null!=gl) { + ShaderUtil.deleteShader(gl, shader()); + } + valid=false; + } + if(null!=shaderBinary) { + shaderBinary.clear(); + shaderBinary=null; + } + shaderSource=null; + shaderBinaryFormat=-1; + shaderType=-1; + id=-1; + } + + @Override + public boolean equals(final Object obj) { + if(this==obj) { return true; } + if(obj instanceof ShaderCode) { + return id()==((ShaderCode)obj).id(); + } + return false; + } + @Override + public int hashCode() { + return id; + } + @Override + public String toString() { + final StringBuilder buf = new StringBuilder("ShaderCode[id="+id+", type="+shaderTypeStr()+", valid="+valid+", shader: "); + for(int i=0; i"); + return; + } + final int sourceCount = shaderSource.length; + final int shaderCount = (null!=shader)?shader.capacity():0; + for(int i=0; i=sourceCount) { + out.println(""); + } else { + final CharSequence[] src = shaderSource[i]; + int lineno=0; + + for(int j=0; jdata after the line containing tag. + *

+ * Note: The shader source to be edit must be created using a mutable StringBuilder. + *

+ * + * @param shaderIdx the shader index to be used. + * @param tag search string + * @param fromIndex start search tag begininig with this index + * @param data the text to be inserted. Shall end with an EOL '\n' character. + * @return index after the inserted data + * + * @throws IllegalStateException if the shader source's CharSequence is immutable, i.e. not of type StringBuilder + */ + public int insertShaderSource(final int shaderIdx, final String tag, final int fromIndex, final CharSequence data) { + if(null==shaderSource) { + throw new IllegalStateException("no shader source"); + } + final int shaderCount = (null!=shader)?shader.capacity():0; + if(0>shaderIdx || shaderIdx>=shaderCount) { + throw new IndexOutOfBoundsException("shaderIdx not within shader bounds [0.."+(shaderCount-1)+"]: "+shaderIdx); + } + final int sourceCount = shaderSource.length; + if(shaderIdx>=sourceCount) { + throw new IndexOutOfBoundsException("shaderIdx not within source bounds [0.."+(sourceCount-1)+"]: "+shaderIdx); + } + final CharSequence[] src = shaderSource[shaderIdx]; + int curEndIndex = 0; + for(int j=0; jeol) { + eol = sb.indexOf("\r", insertIdx); // eol: covers \r 'only' + } + if(0oldName with newName in all shader sources. + *

+ * In case oldName and newName are equal, no action is performed. + *

+ *

+ * Note: The shader source to be edit must be created using a mutable StringBuilder. + *

+ * + * @param oldName the to be replace string + * @param newName the replacement string + * @return the number of replacements + * + * @throws IllegalStateException if the shader source's CharSequence is immutable, i.e. not of type StringBuilder + */ + public int replaceInShaderSource(final String oldName, final String newName) { + if(null==shaderSource) { + throw new IllegalStateException("no shader source"); + } + if(oldName == newName || oldName.equals(newName)) { + return 0; + } + final int oldNameLen = oldName.length(); + final int newNameLen = newName.length(); + int num = 0; + final int sourceCount = shaderSource.length; + for(int shaderIdx = 0; shaderIdxdata at position in shader source for shader shaderIdx. + *

+ * Note: The shader source to be edit must be created using a mutable StringBuilder. + *

+ * + * @param shaderIdx the shader index to be used. + * @param position in shader source segments of shader shaderIdx, -1 will append data + * @param data the text to be inserted. Shall end with an EOL '\n' character + * @return index after the inserted data + * + * @throws IllegalStateException if the shader source's CharSequence is immutable, i.e. not of type StringBuilder + */ + public int insertShaderSource(final int shaderIdx, int position, final CharSequence data) { + if(null==shaderSource) { + throw new IllegalStateException("no shader source"); + } + final int shaderCount = (null!=shader)?shader.capacity():0; + if(0>shaderIdx || shaderIdx>=shaderCount) { + throw new IndexOutOfBoundsException("shaderIdx not within shader bounds [0.."+(shaderCount-1)+"]: "+shaderIdx); + } + final int sourceCount = shaderSource.length; + if(shaderIdx>=sourceCount) { + throw new IndexOutOfBoundsException("shaderIdx not within source bounds [0.."+(sourceCount-1)+"]: "+shaderIdx); + } + final CharSequence[] src = shaderSource[shaderIdx]; + int curEndIndex = 0; + for(int j=0; j position && j == src.length - 1 ) { + position = curEndIndex; + } + if(0 <= position && position <= curEndIndex ) { + sb.insert(position, data); + return position+data.length(); + } + } + return position; + } + + /** + * Adds shader source located in path, + * either relative to the context class or absolute as-is + * at position in shader source for shader shaderIdx. + *

+ * Final location lookup is performed via {@link ClassLoader#getResource(String)} and {@link ClassLoader#getSystemResource(String)}, + * see {@link IOUtil#getResource(Class, String)}. + *

+ *

+ * Note: The shader source to be edit must be created using a mutable StringBuilder. + *

+ * + * @param shaderIdx the shader index to be used. + * @param position in shader source segments of shader shaderIdx, -1 will append data + * @param context class used to help resolve the source location + * @param path location of shader source + * @return index after the inserted code. + * @throws IOException + * @throws IllegalStateException if the shader source's CharSequence is immutable, i.e. not of type StringBuilder + * @see IOUtil#getResource(Class, String) + */ + public int insertShaderSource(final int shaderIdx, final int position, final Class context, final String path) throws IOException { + final CharSequence data = readShaderSource(context, path, true); + if( null != data ) { + return insertShaderSource(shaderIdx, position, data); + } else { + return position; + } + } + + private static int readShaderSource(final Class context, final URLConnection conn, final StringBuilder result, int lineno) throws IOException { + if(DEBUG_CODE) { + if(0 == lineno) { + result.append("// "+conn.getURL().toExternalForm()+"\n"); + } else { + result.append("// included @ line "+lineno+": "+conn.getURL().toExternalForm()+"\n"); + } + } + final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + try { + String line = null; + while ((line = reader.readLine()) != null) { + lineno++; + if (line.startsWith("#include ")) { + final String includeFile; + { + String s = line.substring(9).trim(); + // Bug 1283: Remove shader include filename quotes if exists at start and end only + if( s.startsWith("\"") && s.endsWith("\"")) { + s = s.substring(1, s.length()-1); + } + includeFile = s; + } + URLConnection nextConn = null; + + // Try relative of current shader location + final Uri relUri = Uri.valueOf( conn.getURL() ).getRelativeOf(new Uri.Encoded( includeFile, Uri.PATH_LEGAL )); + nextConn = IOUtil.openURL(relUri.toURL(), "ShaderCode.relativeOf "); + if (nextConn == null) { + // Try relative of class and absolute + nextConn = IOUtil.getResource(includeFile, context.getClassLoader(), context); + } + if (nextConn == null) { + // Fail + throw new FileNotFoundException("Can't find include file " + includeFile); + } + lineno = readShaderSource(context, nextConn, result, lineno); + } else { + result.append(line + "\n"); + } + } + } catch (final URISyntaxException e) { + throw new IOException(e); + } finally { + IOUtil.close(reader, false); + } + return lineno; + } + + /** + * Reads shader source located in conn. + * + * @param context class used to help resolve the source location, may be {@code null} + * @param conn the {@link URLConnection} of the shader source + * @param result {@link StringBuilder} sink for the resulting shader source code + * @throws IOException + */ + public static void readShaderSource(final Class context, final URLConnection conn, final StringBuilder result) throws IOException { + readShaderSource(context, conn, result, 0); + } + + /** + * Reads shader source located in path, + * either relative to the context class or absolute as-is. + *

+ * Final location lookup is performed via {@link ClassLoader#getResource(String)} and {@link ClassLoader#getSystemResource(String)}, + * see {@link IOUtil#getResource(Class, String)}. + *

+ * + * @param context class used to help resolve the source location + * @param path location of shader source + * @param mutableStringBuilder if true method returns a mutable StringBuilder instance + * which can be edited later on at the costs of a String conversion when passing to + * {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)}. + * If false method returns an immutable String instance, + * which can be passed to {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)} + * at no additional costs. + * @throws IOException + * + * @see IOUtil#getResource(Class, String) + */ + public static CharSequence readShaderSource(final Class context, final String path, final boolean mutableStringBuilder) throws IOException { + final URLConnection conn = IOUtil.getResource(path, context.getClassLoader(), context); + if (conn == null) { + return null; + } + final StringBuilder result = new StringBuilder(); + readShaderSource(context, conn, result); + return mutableStringBuilder ? result : result.toString(); + } + + /** + * Reads shader source located from {@link Uri#absolute} {@link Uri} sourceLocation. + * @param sourceLocation {@link Uri} location of shader source + * @param mutableStringBuilder if true method returns a mutable StringBuilder instance + * which can be edited later on at the costs of a String conversion when passing to + * {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)}. + * If false method returns an immutable String instance, + * which can be passed to {@link GL2ES2#glShaderSource(int, int, String[], IntBuffer)} + * at no additional costs. + * @throws IOException + * @since 2.3.2 + */ + public static CharSequence readShaderSource(final Uri sourceLocation, final boolean mutableStringBuilder) throws IOException { + final URLConnection conn = IOUtil.openURL(sourceLocation.toURL(), "ShaderCode "); + if (conn == null) { + return null; + } + final StringBuilder result = new StringBuilder(); + readShaderSource(null, conn, result); + return mutableStringBuilder ? result : result.toString(); + } + + /** + * Reads shader binary located in path, + * either relative to the context class or absolute as-is. + *

+ * Final location lookup is perfomed via {@link ClassLoader#getResource(String)} and {@link ClassLoader#getSystemResource(String)}, + * see {@link IOUtil#getResource(Class, String)}. + *

+ * + * @param context class used to help resolve the source location + * @param path location of shader binary + * @throws IOException + * + * @see IOUtil#getResource(Class, String) + */ + public static ByteBuffer readShaderBinary(final Class context, final String path) throws IOException { + final URLConnection conn = IOUtil.getResource(path, context.getClassLoader(), context); + if (conn == null) { + return null; + } + final BufferedInputStream bis = new BufferedInputStream( conn.getInputStream() ); + try { + return IOUtil.copyStream2ByteBuffer( bis ); + } finally { + IOUtil.close(bis, false); + } + } + + /** + * Reads shader binary located from {@link Uri#absolute} {@link Uri} binLocation. + * @param binLocation {@link Uri} location of shader binary + * @throws IOException + * @since 2.3.2 + */ + public static ByteBuffer readShaderBinary(final Uri binLocation) throws IOException { + final URLConnection conn = IOUtil.openURL(binLocation.toURL(), "ShaderCode "); + if (conn == null) { + return null; + } + final BufferedInputStream bis = new BufferedInputStream( conn.getInputStream() ); + try { + return IOUtil.copyStream2ByteBuffer( bis ); + } finally { + IOUtil.close(bis, false); + } + } + + // Shall we use: #ifdef GL_FRAGMENT_PRECISION_HIGH .. #endif for using highp in fragment shader if avail ? + /** Default precision of {@link GL#isGLES2() ES2} for {@link GL2ES2#GL_VERTEX_SHADER vertex-shader}: {@value #es2_default_precision_vp} */ + public static final String es2_default_precision_vp = "\nprecision highp float;\nprecision highp int;\n/*precision lowp sampler2D;*/\n/*precision lowp samplerCube;*/\n"; + /** Default precision of {@link GL#isGLES2() ES2} for {@link GL2ES2#GL_FRAGMENT_SHADER fragment-shader}: {@value #es2_default_precision_fp} */ + public static final String es2_default_precision_fp = "\nprecision mediump float;\nprecision mediump int;\n/*precision lowp sampler2D;*/\n/*precision lowp samplerCube;*/\n"; + + /** Default precision of {@link GL#isGLES3() ES3} for {@link GL2ES2#GL_VERTEX_SHADER vertex-shader}: {@value #es3_default_precision_vp} */ + public static final String es3_default_precision_vp = es2_default_precision_vp; + /** + * Default precision of {@link GL#isGLES3() ES3} for {@link GL2ES2#GL_FRAGMENT_SHADER fragment-shader}: {@value #es3_default_precision_fp}, + * same as for {@link GL2ES2#GL_VERTEX_SHADER vertex-shader}, i.e {@link #es3_default_precision_vp}, + * due to ES 3.x requirements of using same precision for uniforms! + */ + public static final String es3_default_precision_fp = es3_default_precision_vp; + + /** Default precision of GLSL ≥ 1.30 as required until < 1.50 for {@link GL2ES2#GL_VERTEX_SHADER vertex-shader} or {@link GL3#GL_GEOMETRY_SHADER geometry-shader}: {@value #gl3_default_precision_vp_gp}. See GLSL Spec 1.30-1.50 Section 4.5.3. */ + public static final String gl3_default_precision_vp_gp = "\nprecision highp float;\nprecision highp int;\n"; + /** Default precision of GLSL ≥ 1.30 as required until < 1.50 for {@link GL2ES2#GL_FRAGMENT_SHADER fragment-shader}: {@value #gl3_default_precision_fp}. See GLSL Spec 1.30-1.50 Section 4.5.3. */ + public static final String gl3_default_precision_fp = "\nprecision highp float;\nprecision mediump int;\n/*precision mediump sampler2D;*/\n"; + + /** Behavior for GLSL extension directive, see {@link #createExtensionDirective(String, String)}, value {@value}. */ + public static final String REQUIRE = "require"; + /** Behavior for GLSL extension directive, see {@link #createExtensionDirective(String, String)}, value {@value}. */ + public static final String ENABLE = "enable"; + /** Behavior for GLSL extension directive, see {@link #createExtensionDirective(String, String)}, value {@value}. */ + public static final String DISABLE = "disable"; + /** Behavior for GLSL extension directive, see {@link #createExtensionDirective(String, String)}, value {@value}. */ + public static final String WARN = "warn"; + + /** + * Creates a GLSL extension directive. + *

+ * Prefer {@link #ENABLE} over {@link #REQUIRE}, since the latter will force a failure if not supported. + *

+ * + * @param extensionName + * @param behavior shall be either {@link #REQUIRE}, {@link #ENABLE}, {@link #DISABLE} or {@link #WARN} + * @return the complete extension directive + */ + public static String createExtensionDirective(final String extensionName, final String behavior) { + return "#extension " + extensionName + " : " + behavior + "\n"; + } + + /** + * Add GLSL version at the head of this shader source code. + *

+ * Note: The shader source to be edit must be created using a mutable StringBuilder. + *

+ * @param gl a GL context, which must have been made current once + * @return the index after the inserted data, maybe 0 if nothing has be inserted. + */ + public final int addGLSLVersion(final GL2ES2 gl) { + return insertShaderSource(0, 0, gl.getContext().getGLSLVersionString()); + } + + /** + * Adds default precision to source code at given position if required, i.e. + * {@link #es2_default_precision_vp}, {@link #es2_default_precision_fp}, + * {@link #gl3_default_precision_vp_gp}, {@link #gl3_default_precision_fp} or none, + * depending on the {@link GLContext#getGLSLVersionNumber() GLSL version} being used. + *

+ * Note: The shader source to be edit must be created using a mutable StringBuilder. + *

+ * @param gl a GL context, which must have been made current once + * @param pos position within this mutable shader source. + * @return the index after the inserted data, maybe 0 if nothing has be inserted. + */ + public final int addDefaultShaderPrecision(final GL2ES2 gl, int pos) { + final String defaultPrecision; + if( gl.isGLES3() ) { + switch ( shaderType ) { + case GL2ES2.GL_VERTEX_SHADER: + defaultPrecision = es3_default_precision_vp; break; + case GL2ES2.GL_FRAGMENT_SHADER: + defaultPrecision = es3_default_precision_fp; break; + case GL3ES3.GL_COMPUTE_SHADER: + defaultPrecision = es3_default_precision_fp; break; + default: + defaultPrecision = null; + break; + } + } else if( gl.isGLES2() ) { + switch ( shaderType ) { + case GL2ES2.GL_VERTEX_SHADER: + defaultPrecision = es2_default_precision_vp; break; + case GL2ES2.GL_FRAGMENT_SHADER: + defaultPrecision = es2_default_precision_fp; break; + default: + defaultPrecision = null; + break; + } + } else if( requiresGL3DefaultPrecision(gl) ) { + // GLSL [ 1.30 .. 1.50 [ needs at least fragement float default precision! + switch ( shaderType ) { + case GL2ES2.GL_VERTEX_SHADER: + case GL3ES3.GL_GEOMETRY_SHADER: + case GL3ES3.GL_TESS_CONTROL_SHADER: + case GL3ES3.GL_TESS_EVALUATION_SHADER: + defaultPrecision = gl3_default_precision_vp_gp; break; + case GL2ES2.GL_FRAGMENT_SHADER: + defaultPrecision = gl3_default_precision_fp; break; + case GL3ES3.GL_COMPUTE_SHADER: + defaultPrecision = gl3_default_precision_fp; break; + default: + defaultPrecision = null; + break; + } + } else { + defaultPrecision = null; + } + if( null != defaultPrecision ) { + pos = insertShaderSource(0, pos, defaultPrecision); + } + return pos; + } + + /** Returns true, if GLSL version requires default precision, i.e. ES2 or GLSL [1.30 .. 1.50[. */ + public static final boolean requiresDefaultPrecision(final GL2ES2 gl) { + if( gl.isGLES() ) { + return true; + } + return requiresGL3DefaultPrecision(gl); + } + + /** Returns true, if GL3 GLSL version requires default precision, i.e. GLSL [1.30 .. 1.50[. */ + public static final boolean requiresGL3DefaultPrecision(final GL2ES2 gl) { + if( gl.isGL3() ) { + final VersionNumber glslVersion = gl.getContext().getGLSLVersionNumber(); + return glslVersion.compareTo(GLContext.Version1_30) >= 0 && glslVersion.compareTo(GLContext.Version1_50) < 0 ; + } else { + return false; + } + } + + /** + * Default customization of this shader source code. + *

+ * Note: The shader source to be edit must be created using a mutable StringBuilder. + *

+ * @param gl a GL context, which must have been made current once + * @param preludeVersion if true {@link GLContext#getGLSLVersionString()} is preluded, otherwise not. + * @param addDefaultPrecision if true default precision source code line(s) are added, i.e. + * {@link #es2_default_precision_vp}, {@link #es2_default_precision_fp}, + * {@link #gl3_default_precision_vp_gp}, {@link #gl3_default_precision_fp} or none, + * depending on the {@link GLContext#getGLSLVersionNumber() GLSL version} being used. + * @return the index after the inserted data, maybe 0 if nothing has be inserted. + * @see #addGLSLVersion(GL2ES2) + * @see #addDefaultShaderPrecision(GL2ES2, int) + */ + public final int defaultShaderCustomization(final GL2ES2 gl, final boolean preludeVersion, final boolean addDefaultPrecision) { + int pos; + if( preludeVersion ) { + pos = addGLSLVersion(gl); + } else { + pos = 0; + } + if( addDefaultPrecision ) { + pos = addDefaultShaderPrecision(gl, pos); + } + return pos; + } + + /** + * Default customization of this shader source code. + *

+ * Note: The shader source to be edit must be created using a mutable StringBuilder. + *

+ * @param gl a GL context, which must have been made current once + * @param preludeVersion if true {@link GLContext#getGLSLVersionString()} is preluded, otherwise not. + * @param esDefaultPrecision optional default precision source code line(s) preluded if not null and if {@link GL#isGLES()}. + * You may use {@link #es2_default_precision_fp} for fragment shader and {@link #es2_default_precision_vp} for vertex shader. + * @return the index after the inserted data, maybe 0 if nothing has be inserted. + * @see #addGLSLVersion(GL2ES2) + * @see #addDefaultShaderPrecision(GL2ES2, int) + */ + public final int defaultShaderCustomization(final GL2ES2 gl, final boolean preludeVersion, final String esDefaultPrecision) { + int pos; + if( preludeVersion ) { + pos = addGLSLVersion(gl); + } else { + pos = 0; + } + if( gl.isGLES() && null != esDefaultPrecision ) { + pos = insertShaderSource(0, pos, esDefaultPrecision); + } else { + pos = addDefaultShaderPrecision(gl, pos); + } + return pos; + } + + //---------------------------------------------------------------------- + // Internals only below this point + // + + protected CharSequence[][] shaderSource = null; + protected Buffer shaderBinary = null; + protected int shaderBinaryFormat = -1; + protected IntBuffer shader = null; + protected int shaderType = -1; + protected int id = -1; + + protected boolean valid=false; + + private static synchronized int getNextID() { + return nextID++; + } + protected static int nextID = 1; +} + diff --git a/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderProgram.java b/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderProgram.java new file mode 100644 index 00000000..d09a3685 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderProgram.java @@ -0,0 +1,321 @@ +/** + * Copyright 2010 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ + +package net.opengrabeso.opengl.util.glsl; + +import com.jogamp.opengl.*; + +import com.jogamp.common.os.Platform; + +import java.util.HashSet; +import java.util.Iterator; +import java.io.PrintStream; + +public class ShaderProgram { + + public ShaderProgram() { + id = getNextID(); + } + + public boolean linked() { + return programLinked; + } + + public boolean inUse() { + return programInUse; + } + + /** Returns the shader program name, which is non zero if valid. */ + public int program() { return shaderProgram; } + + /** + * returns the uniq shader id as an integer + */ + public int id() { return id; } + + /** + * Detaches all shader codes and deletes the program. + * Destroys the shader codes as well. + * Calls release(gl, true) + * + * @see #release(GL2ES2, boolean) + */ + public synchronized void destroy(final GL2ES2 gl) { + release(gl, true); + } + + /** + * Detaches all shader codes and deletes the program, + * but leaves the shader code intact. + * Calls release(gl, false) + * + * @see #release(GL2ES2, boolean) + */ + public synchronized void release(final GL2ES2 gl) { + release(gl, false); + } + + /** + * Detaches all shader codes and deletes the program. + * If destroyShaderCode is true it destroys the shader codes as well. + */ + public synchronized void release(final GL2ES2 gl, final boolean destroyShaderCode) { + if( programLinked ) { + useProgram(gl, false); + } + for(final Iterator iter=allShaderCode.iterator(); iter.hasNext(); ) { + final ShaderCode shaderCode = iter.next(); + if(attachedShaderCode.remove(shaderCode)) { + ShaderUtil.detachShader(gl, shaderProgram, shaderCode.shader()); + } + if(destroyShaderCode) { + shaderCode.destroy(gl); + } + } + allShaderCode.clear(); + attachedShaderCode.clear(); + if( 0 != shaderProgram ) { + gl.glDeleteProgram(shaderProgram); + shaderProgram=0; + } + } + + // + // ShaderCode handling + // + + /** + * Adds a new shader to this program. + * + *

This command does not compile and attach the shader + */ + public synchronized void add(final ShaderCode shaderCode) { + allShaderCode.add(shaderCode); + } + + public synchronized boolean contains(final ShaderCode shaderCode) { + return allShaderCode.contains(shaderCode); + } + + /** + * Warning slow O(n) operation .. + * @param id + * @return + */ + public synchronized ShaderCode getShader(final int id) { + for(final Iterator iter=allShaderCode.iterator(); iter.hasNext(); ) { + final ShaderCode shaderCode = iter.next(); + if(shaderCode.id() == id) { + return shaderCode; + } + } + return null; + } + + // + // ShaderCode / Program handling + // + + /** + * Creates the empty GL program object using {@link GL2ES2#glCreateProgram()}, + * if not already created. + * + * @param gl + * @return true if shader program is valid, i.e. not zero + */ + public synchronized final boolean init(final GL2ES2 gl) { + if( 0 == shaderProgram ) { + shaderProgram = gl.glCreateProgram(); + } + return 0 != shaderProgram; + } + + /** + * Adds a new shader to a this non running program. + * + *

Compiles and attaches the shader, if not done yet.

+ * + * @return true if the shader was successfully added, false if compilation failed. + */ + public synchronized boolean add(final GL2ES2 gl, final ShaderCode shaderCode, final PrintStream verboseOut) { + if( !init(gl) ) { return false; } + if( allShaderCode.add(shaderCode) ) { + if( !shaderCode.compile(gl, verboseOut) ) { + return false; + } + if( attachedShaderCode.add(shaderCode) ) { + ShaderUtil.attachShader(gl, shaderProgram, shaderCode.shader()); + } + } + return true; + } + + /** + * Replace a shader in a program and re-links the program. + * + * @param gl + * @param oldShader the to be replace Shader + * @param newShader the new ShaderCode + * @param verboseOut the optional verbose output stream + * + * @return true if all steps are valid, shader compilation, attachment and linking; otherwise false. + */ + public synchronized boolean replaceShader(final GL2ES2 gl, final ShaderCode oldShader, final ShaderCode newShader, final PrintStream verboseOut) { + if(!init(gl) || !newShader.compile(gl, verboseOut)) { + return false; + } + + final boolean shaderWasInUse = inUse(); + if(shaderWasInUse) { + useProgram(gl, false); + } + + if(null != oldShader && allShaderCode.remove(oldShader)) { + if(attachedShaderCode.remove(oldShader)) { + ShaderUtil.detachShader(gl, shaderProgram, oldShader.shader()); + } + } + + add(newShader); + if(attachedShaderCode.add(newShader)) { + ShaderUtil.attachShader(gl, shaderProgram, newShader.shader()); + } + + gl.glLinkProgram(shaderProgram); + + programLinked = ShaderUtil.isProgramLinkStatusValid(gl, shaderProgram, verboseOut); + if ( programLinked && shaderWasInUse ) { + useProgram(gl, true); + } + return programLinked; + } + + /** + * Links the shader code to the program. + * + *

Compiles and attaches the shader code to the program if not done by yet

+ * + *

Within this process, all GL resources (shader and program objects) are created if necessary.

+ * + * @param gl + * @param verboseOut + * @return true if program was successfully linked and is valid, otherwise false + * + * @see #init(GL2ES2) + */ + public synchronized boolean link(final GL2ES2 gl, final PrintStream verboseOut) { + if( !init(gl) ) { + programLinked = false; // mark unlinked due to user attempt to [re]link + return false; + } + + for(final Iterator iter=allShaderCode.iterator(); iter.hasNext(); ) { + final ShaderCode shaderCode = iter.next(); + if(!shaderCode.compile(gl, verboseOut)) { + programLinked = false; // mark unlinked due to user attempt to [re]link + return false; + } + if(attachedShaderCode.add(shaderCode)) { + ShaderUtil.attachShader(gl, shaderProgram, shaderCode.shader()); + } + } + + // Link the program + gl.glLinkProgram(shaderProgram); + + programLinked = ShaderUtil.isProgramLinkStatusValid(gl, shaderProgram, verboseOut); + + return programLinked; + } + + @Override + public boolean equals(final Object obj) { + if(this == obj) { return true; } + if(obj instanceof ShaderProgram) { + return id()==((ShaderProgram)obj).id(); + } + return false; + } + + @Override + public int hashCode() { + return id; + } + + public StringBuilder toString(StringBuilder sb) { + if(null == sb) { + sb = new StringBuilder(); + } + sb.append("ShaderProgram[id=").append(id); + sb.append(", linked="+programLinked+", inUse="+programInUse+", program: "+shaderProgram+","); + for(final Iterator iter=allShaderCode.iterator(); iter.hasNext(); ) { + sb.append(Platform.getNewline()).append(" ").append(iter.next()); + } + sb.append("]"); + return sb; + } + + @Override + public String toString() { + return toString(null).toString(); + } + + /** + * Performs {@link GL2ES2#glValidateProgram(int)} via {@link ShaderUtil#isProgramExecStatusValid(GL, int, PrintStream)}. + * @see ShaderUtil#isProgramExecStatusValid(GL, int, PrintStream) + **/ + public synchronized boolean validateProgram(final GL2ES2 gl, final PrintStream verboseOut) { + return ShaderUtil.isProgramExecStatusValid(gl, shaderProgram, verboseOut); + } + + public synchronized void useProgram(final GL2ES2 gl, boolean on) { + if(!programLinked) { throw new GLException("Program is not linked"); } + if(programInUse==on) { return; } + if( 0 == shaderProgram ) { + on = false; + } + gl.glUseProgram( on ? shaderProgram : 0 ); + programInUse = on; + } + public synchronized void notifyNotInUse() { + programInUse = false; + } + + private boolean programLinked = false; + private boolean programInUse = false; + private int shaderProgram = 0; // non zero is valid! + private final HashSet allShaderCode = new HashSet(); + private final HashSet attachedShaderCode = new HashSet(); + private final int id; + + private static synchronized int getNextID() { + return nextID++; + } + private static int nextID = 1; +} + diff --git a/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderState.java b/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderState.java new file mode 100644 index 00000000..4f8fe85d --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderState.java @@ -0,0 +1,976 @@ +/** + * Copyright 2010 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ + +package net.opengrabeso.opengl.util.glsl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import com.jogamp.opengl.*; + +import com.jogamp.common.ExceptionUtils; +import com.jogamp.common.os.Platform; + +/** + * ShaderState allows to sharing data between shader programs, + * while updating the attribute and uniform locations when switching. + *

+ * This allows seamless switching of programs using almost same data + * but performing different artifacts. + *

+ *

+ * A {@link #useProgram(GL2ES2, boolean) used} ShaderState is attached to the current GL context + * and can be retrieved via {@link #getShaderState(GL)}. + *

+ */ +public class ShaderState { + public static final boolean DEBUG = false; + + public ShaderState() { + } + + public boolean verbose() { return verbose; } + + public void setVerbose(final boolean v) { verbose = DEBUG || v; } + + /** + * Returns the attached user object for the given name to this ShaderState. + */ + public final Object getAttachedObject(final String name) { + return attachedObjectsByString.get(name); + } + + /** + * Attach user object for the given name to this ShaderState. + * Returns the previously set object or null. + * + * @return the previous mapped object or null if none + */ + public final Object attachObject(final String name, final Object obj) { + return attachedObjectsByString.put(name, obj); + } + + /** + * @param name name of the mapped object to detach + * + * @return the previous mapped object or null if none + */ + public final Object detachObject(final String name) { + return attachedObjectsByString.remove(name); + } + + /** + * Turns the shader program on or off.
+ * + + * + * @see com.jogamp.opengl.util.glsl.ShaderState#useProgram(GL2ES2, boolean) + */ + public synchronized void useProgram(final GL2ES2 gl, final boolean on) { + if(null==shaderProgram) { throw new GLException("No program is attached"); } + if(on) { + if(shaderProgram.linked()) { + shaderProgram.useProgram(gl, true); + if(resetAllShaderData) { + resetAllAttributes(gl); + resetAllUniforms(gl); + } + } else { + if(resetAllShaderData) { + setAllAttributes(gl); + } + if(!shaderProgram.link(gl, System.err)) { + throw new GLException("could not link program: "+shaderProgram); + } + shaderProgram.useProgram(gl, true); + if(resetAllShaderData) { + resetAllUniforms(gl); + } + } + resetAllShaderData = false; + } else { + shaderProgram.useProgram(gl, false); + } + } + + public boolean linked() { + return (null!=shaderProgram)?shaderProgram.linked():false; + } + + public boolean inUse() { + return (null!=shaderProgram)?shaderProgram.inUse():false; + } + + /** + * Attach or switch a shader program + * + *

Attaching a shader program the first time, + * as well as switching to another program on the fly, + * while managing all attribute and uniform data.

+ * + *

[Re]sets all data and use program in case of a program switch.

+ * + *

Use program, {@link #useProgram(GL2ES2, boolean)}, + * if enable is true.

+ * + * @return true if shader program was attached, otherwise false (already attached) + * + + */ + public synchronized boolean attachShaderProgram(final GL2ES2 gl, final ShaderProgram prog, final boolean enable) { + if(verbose) { + final int curId = (null!=shaderProgram)?shaderProgram.id():-1; + final int newId = (null!=prog)?prog.id():-1; + System.err.println("ShaderState: attachShaderProgram: "+curId+" -> "+newId+" (enable: "+enable+")\n\t"+shaderProgram+"\n\t"+prog); + if(DEBUG) { + ExceptionUtils.dumpStack(System.err); + } + } + if(null!=shaderProgram) { + if(shaderProgram.equals(prog)) { + if(enable) { + useProgram(gl, true); + } + // nothing else to do .. + if(verbose) { + System.err.println("ShaderState: attachShaderProgram: No switch, equal id: "+shaderProgram.id()+", enabling "+enable); + } + return false; + } + if(shaderProgram.inUse()) { + if(null != prog && enable) { + shaderProgram.notifyNotInUse(); + } else { + // no new 'enabled' program - disable + useProgram(gl, false); + } + } + resetAllShaderData = true; + } + + // register new one + shaderProgram = prog; + + if(null!=shaderProgram) { + // [re]set all data and use program if switching program, + // or use program if program is linked + if(resetAllShaderData || enable) { + useProgram(gl, true); // may reset all data + if(!enable) { + useProgram(gl, false); + } + } + } + if(DEBUG) { + System.err.println("Info: attachShaderProgram: END"); + } + return true; + } + + public ShaderProgram shaderProgram() { return shaderProgram; } + + /** + * Calls {@link #release(GL2ES2, boolean, boolean, boolean) release(gl, true, true, true)} + * @see #release(GL2ES2, boolean, boolean, boolean) + */ + public synchronized void destroy(final GL2ES2 gl) { + release(gl, true, true, true); + attachedObjectsByString.clear(); + } + + /** + * Calls {@link #release(GL2ES2, boolean, boolean, boolean) release(gl, false, false, false)} + * @see #release(GL2ES2, boolean, boolean, boolean) + */ + public synchronized void releaseAllData(final GL2ES2 gl) { + release(gl, false, false, false); + } + + /** + * @see ShaderProgram#release(GL2ES2, boolean) + */ + public synchronized void release(final GL2ES2 gl, final boolean destroyBoundAttributes, final boolean destroyShaderProgram, final boolean destroyShaderCode) { + if(null!=shaderProgram && shaderProgram.linked() ) { + shaderProgram.useProgram(gl, false); + } + if(destroyBoundAttributes) { + for(final Iterator iter = managedAttributes.iterator(); iter.hasNext(); ) { + iter.next().destroy(gl); + } + } + releaseAllAttributes(gl); + releaseAllUniforms(gl); + if(null!=shaderProgram && destroyShaderProgram) { + shaderProgram.release(gl, destroyShaderCode); + } + } + + // + // Shader attribute handling + // + + /** + * Gets the cached location of a shader attribute. + * + * @return -1 if there is no such attribute available, + * otherwise >= 0 + * + * @see #bindAttribLocation(GL2ES2, int, String) + * @see #bindAttribLocation(GL2ES2, int, GLArrayData) + * @see #getAttribLocation(GL2ES2, String) + * @see GL2ES2#glGetAttribLocation(int, String) + */ + public int getCachedAttribLocation(final String name) { + final Integer idx = activeAttribLocationMap.get(name); + return (null!=idx)?idx.intValue():-1; + } + + /** + * Get the previous cached vertex attribute data. + * + * @return the GLArrayData object, null if not previously set. + * + * @see #ownAttribute(GLArrayData, boolean) + */ + public GLArrayData getAttribute(final String name) { + return activeAttribDataMap.get(name); + } + + public boolean isActiveAttribute(final GLArrayData attribute) { + return attribute == activeAttribDataMap.get(attribute.getName()); + } + + /** + * Binds or unbinds the {@link GLArrayData} lifecycle to this ShaderState. + * + *

If an attribute location is cached (ie {@link #bindAttribLocation(GL2ES2, int, String)}) + * it is promoted to the {@link GLArrayData} instance.

+ * + *

The attribute will be destroyed with {@link #destroy(GL2ES2)} + * and it's location will be reset when switching shader with {@link #attachShaderProgram(GL2ES2, ShaderProgram)}.

+ * + *

The data will not be transfered to the GPU, use {@link #vertexAttribPointer(GL2ES2, GLArrayData)} additionally.

+ * + *

The data will also be {@link GLArrayData#associate(Object, boolean) associated} with this ShaderState.

+ * + * @param attribute the {@link GLArrayData} which lifecycle shall be managed + * @param own true if owning shall be performs, false if disowning. + * + * @see #bindAttribLocation(GL2ES2, int, String) + * @see #getAttribute(String) + * @see GLArrayData#associate(Object, boolean) + */ + public void ownAttribute(final GLArrayData attribute, final boolean own) { + if(own) { + final int location = getCachedAttribLocation(attribute.getName()); + if(0<=location) { + attribute.setLocation(location); + } + managedAttributes.add(managedAttributes.size(), attribute); + } else { + managedAttributes.remove(attribute); + } + attribute.associate(this, own); + } + + public boolean ownsAttribute(final GLArrayData attribute) { + return managedAttributes.contains(attribute); + } + + /** + * Binds a shader attribute to a location. + * Multiple names can be bound to one location. + * The value will be cached and can be retrieved via {@link #getCachedAttribLocation(String)} + * before or after linking. + * + + + * + * @see GL2ES2#glBindAttribLocation(int, int, String) + * @see #getAttribLocation(GL2ES2, String) + * @see #getCachedAttribLocation(String) + */ + public void bindAttribLocation(final GL2ES2 gl, final int location, final String name) { + if(null==shaderProgram) throw new GLException("No program is attached"); + if(shaderProgram.linked()) throw new GLException("Program is already linked"); + activeAttribLocationMap.put(name, Integer.valueOf(location)); + gl.glBindAttribLocation(shaderProgram.program(), location, name); + } + + /** + * Binds a shader {@link GLArrayData} attribute to a location. + * Multiple names can be bound to one location. + * The value will be cached and can be retrieved via {@link #getCachedAttribLocation(String)} + * and {@link #getAttribute(String)}before or after linking. + * The {@link GLArrayData}'s location will be set as well. + * + + + * + * @see GL2ES2#glBindAttribLocation(int, int, String) + * @see #getAttribLocation(GL2ES2, String) + * @see #getCachedAttribLocation(String) + * @see #getAttribute(String) + */ + public void bindAttribLocation(final GL2ES2 gl, final int location, final GLArrayData data) { + if(null==shaderProgram) throw new GLException("No program is attached"); + if(shaderProgram.linked()) throw new GLException("Program is already linked"); + final String name = data.getName(); + activeAttribLocationMap.put(name, Integer.valueOf(location)); + data.setLocation(gl, shaderProgram.program(), location); + activeAttribDataMap.put(data.getName(), data); + } + + /** + * Gets the location of a shader attribute with given name.
+ * Uses either the cached value {@link #getCachedAttribLocation(String)} if valid, + * or the GLSL queried via {@link GL2ES2#glGetAttribLocation(int, String)}.
+ * The location will be cached. + * + * @return -1 if there is no such attribute available, + * otherwise >= 0 + + + * + * @see #getCachedAttribLocation(String) + * @see #bindAttribLocation(GL2ES2, int, GLArrayData) + * @see #bindAttribLocation(GL2ES2, int, String) + * @see GL2ES2#glGetAttribLocation(int, String) + */ + public int getAttribLocation(final GL2ES2 gl, final String name) { + if(null==shaderProgram) throw new GLException("No program is attached"); + int location = getCachedAttribLocation(name); + if(0>location) { + if(!shaderProgram.linked()) throw new GLException("Program is not linked"); + location = gl.glGetAttribLocation(shaderProgram.program(), name); + if(0<=location) { + activeAttribLocationMap.put(name, Integer.valueOf(location)); + if(DEBUG) { + System.err.println("ShaderState: glGetAttribLocation: "+name+", loc: "+location); + } + } else if(verbose) { + System.err.println("ShaderState: glGetAttribLocation failed, no location for: "+name+", loc: "+location); + if(DEBUG) { + ExceptionUtils.dumpStack(System.err); + } + } + } + return location; + } + + /** + * Validates and returns the location of a shader attribute.
+ * Uses either the cached value {@link #getCachedAttribLocation(String)} if valid, + * or the GLSL queried via {@link GL2ES2#glGetAttribLocation(int, String)}.
+ * The location will be cached and set in the + * {@link GLArrayData} object. + * + * @return -1 if there is no such attribute available, + * otherwise >= 0 + * + + + * + * @see #getCachedAttribLocation(String) + * @see #bindAttribLocation(GL2ES2, int, GLArrayData) + * @see #bindAttribLocation(GL2ES2, int, String) + * @see GL2ES2#glGetAttribLocation(int, String) + * @see #getAttribute(String) + */ + public int getAttribLocation(final GL2ES2 gl, final GLArrayData data) { + if(null==shaderProgram) throw new GLException("No program is attached"); + final String name = data.getName(); + int location = getCachedAttribLocation(name); + if(0<=location) { + data.setLocation(location); + } else { + if(!shaderProgram.linked()) throw new GLException("Program is not linked"); + location = data.setLocation(gl, shaderProgram.program()); + if(0<=location) { + activeAttribLocationMap.put(name, Integer.valueOf(location)); + if(DEBUG) { + System.err.println("ShaderState: glGetAttribLocation: "+name+", loc: "+location); + } + } else if(verbose) { + System.err.println("ShaderState: glGetAttribLocation failed, no location for: "+name+", loc: "+location); + if(DEBUG) { + ExceptionUtils.dumpStack(System.err); + } + } + } + activeAttribDataMap.put(data.getName(), data); + return location; + } + + // + // Enabled Vertex Arrays and its data + // + + /** + * @return true if the named attribute is enable + */ + public final boolean isVertexAttribArrayEnabled(final String name) { + final Boolean v = activedAttribEnabledMap.get(name); + return null != v && v.booleanValue(); + } + + /** + * @return true if the {@link GLArrayData} attribute is enable + */ + public final boolean isVertexAttribArrayEnabled(final GLArrayData data) { + return isVertexAttribArrayEnabled(data.getName()); + } + + private boolean enableVertexAttribArray(final GL2ES2 gl, final String name, int location) { + activedAttribEnabledMap.put(name, Boolean.TRUE); + if(0>location) { + location = getAttribLocation(gl, name); + if(0>location) { + if(verbose) { + System.err.println("ShaderState: glEnableVertexAttribArray failed, no index for: "+name); + if(DEBUG) { + ExceptionUtils.dumpStack(System.err); + } + } + return false; + } + } + if(DEBUG) { + System.err.println("ShaderState: glEnableVertexAttribArray: "+name+", loc: "+location); + } + gl.glEnableVertexAttribArray(location); + return true; + } + + /** + * Enables a vertex attribute array. + * + * This method retrieves the the location via {@link #getAttribLocation(GL2ES2, GLArrayData)} + * hence {@link #enableVertexAttribArray(GL2ES2, GLArrayData)} shall be preferred. + * + * Even if the attribute is not found in the current shader, + * it is marked enabled in this state. + * + * @return false, if the name is not found, otherwise true + * + + */ + public boolean enableVertexAttribArray(final GL2ES2 gl, final String name) { + return enableVertexAttribArray(gl, name, -1); + } + + + /** + * Enables a vertex attribute array. + * + * This method uses the {@link GLArrayData}'s location if set + * and is the preferred alternative to {@link #enableVertexAttribArray(GL2ES2, String)}. + * If data location is unset it will be retrieved via {@link #getAttribLocation(GL2ES2, GLArrayData)} set + * and cached in this state. + * + * Even if the attribute is not found in the current shader, + * it is marked enabled in this state. + * + * @return false, if the name is not found, otherwise true + * + + */ + public boolean enableVertexAttribArray(final GL2ES2 gl, final GLArrayData data) { + if(0 > data.getLocation()) { + getAttribLocation(gl, data); + } else { + // ensure data is the current bound one + activeAttribDataMap.put(data.getName(), data); + } + return enableVertexAttribArray(gl, data.getName(), data.getLocation()); + } + + private boolean disableVertexAttribArray(final GL2ES2 gl, final String name, int location) { + activedAttribEnabledMap.put(name, Boolean.FALSE); + if(0>location) { + location = getAttribLocation(gl, name); + if(0>location) { + if(verbose) { + System.err.println("ShaderState: glDisableVertexAttribArray failed, no index for: "+name); + if(DEBUG) { + ExceptionUtils.dumpStack(System.err); + } + } + return false; + } + } + if(DEBUG) { + System.err.println("ShaderState: glDisableVertexAttribArray: "+name); + } + gl.glDisableVertexAttribArray(location); + return true; + } + + /** + * Disables a vertex attribute array + * + * This method retrieves the the location via {@link #getAttribLocation(GL2ES2, GLArrayData)} + * hence {@link #disableVertexAttribArray(GL2ES2, GLArrayData)} shall be preferred. + * + * Even if the attribute is not found in the current shader, + * it is removed from this state enabled list. + * + * @return false, if the name is not found, otherwise true + * + + + */ + public boolean disableVertexAttribArray(final GL2ES2 gl, final String name) { + return disableVertexAttribArray(gl, name, -1); + } + + /** + * Disables a vertex attribute array + * + * This method uses the {@link GLArrayData}'s location if set + * and is the preferred alternative to {@link #disableVertexAttribArray(GL2ES2, String)}. + * If data location is unset it will be retrieved via {@link #getAttribLocation(GL2ES2, GLArrayData)} set + * and cached in this state. + * + * Even if the attribute is not found in the current shader, + * it is removed from this state enabled list. + * + * @return false, if the name is not found, otherwise true + * + + + */ + public boolean disableVertexAttribArray(final GL2ES2 gl, final GLArrayData data) { + if(0 > data.getLocation()) { + getAttribLocation(gl, data); + } + return disableVertexAttribArray(gl, data.getName(), data.getLocation()); + } + + /** + * Set the {@link GLArrayData} vertex attribute data, if it's location is valid, i.e. ≥ 0. + *

+ * This method uses the {@link GLArrayData}'s location if valid, i.e. ≥ 0.
+ * If data's location is invalid, it will be retrieved via {@link #getAttribLocation(GL2ES2, GLArrayData)}, + * set and cached in this state. + *

+ * + * @return false, if the location could not be determined, otherwise true + * + + + */ + public boolean vertexAttribPointer(final GL2ES2 gl, final GLArrayData data) { + int location = data.getLocation(); + if(0 > location) { + location = getAttribLocation(gl, data); + } + if(0 <= location) { + // only pass the data, if the attribute exists in the current shader + if(DEBUG) { + System.err.println("ShaderState: glVertexAttribPointer: "+data); + } + gl.glVertexAttribPointer(data); + return true; + } + return false; + } + + /** + * Releases all mapped vertex attribute data, + * disables all enabled attributes and loses all indices + */ + public void releaseAllAttributes(final GL2ES2 gl) { + if(null!=shaderProgram) { + for(final Iterator iter = activeAttribDataMap.values().iterator(); iter.hasNext(); ) { + disableVertexAttribArray(gl, iter.next()); + } + for(final Iterator iter = activedAttribEnabledMap.keySet().iterator(); iter.hasNext(); ) { + disableVertexAttribArray(gl, iter.next()); + } + } + activeAttribDataMap.clear(); + activedAttribEnabledMap.clear(); + activeAttribLocationMap.clear(); + managedAttributes.clear(); + } + + /** + * Disables all vertex attribute arrays. + * + * Their enabled stated will be removed from this state only + * if 'removeFromState' is true. + * + * This method purpose is more for debugging. + */ + public void disableAllVertexAttributeArrays(final GL2ES2 gl, final boolean removeFromState) { + for(final Iterator iter = activedAttribEnabledMap.keySet().iterator(); iter.hasNext(); ) { + final String name = iter.next(); + if(removeFromState) { + activedAttribEnabledMap.remove(name); + } + final int index = getAttribLocation(gl, name); + if(0<=index) { + gl.glDisableVertexAttribArray(index); + } + } + } + + private final void relocateAttribute(final GL2ES2 gl, final GLArrayData attribute) { + // get new location .. note: 'activeAttribLocationMap' is cleared before + final String name = attribute.getName(); + final int loc = attribute.setLocation(gl, shaderProgram.program()); + if(0<=loc) { + activeAttribLocationMap.put(name, Integer.valueOf(loc)); + if(DEBUG) { + System.err.println("ShaderState: relocateAttribute: "+name+", loc: "+loc); + } + if(isVertexAttribArrayEnabled(name)) { + // enable attrib, VBO and pass location/data + gl.glEnableVertexAttribArray(loc); + } + + if( attribute.isVBO() ) { + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, attribute.getVBOName()); + gl.glVertexAttribPointer(attribute); + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0); + } else { + gl.glVertexAttribPointer(attribute); + } + } + } + + /** + * Reset all previously enabled mapped vertex attribute data. + * + *

+ * Attribute data is bound to the GL state, i.e. VBO data itself will not be updated. + *

+ * + *

+ * Attribute location and it's data assignment is bound to the program, + * hence both are updated. + *

+ * + *

+ * Note: Such update could only be prevented, + * if tracking am attribute/program dirty flag. + *

+ * + + * + * @see #attachShaderProgram(GL2ES2, ShaderProgram) + */ + private final void resetAllAttributes(final GL2ES2 gl) { + if(!shaderProgram.linked()) throw new GLException("Program is not linked"); + activeAttribLocationMap.clear(); + + for(int i=0; i iter = activeAttribDataMap.values().iterator(); iter.hasNext(); ) { + relocateAttribute(gl, iter.next()); + } + } + + private final void setAttribute(final GL2ES2 gl, final GLArrayData attribute) { + // get new location .. + final String name = attribute.getName(); + final int loc = attribute.getLocation(); + + if(0<=loc) { + bindAttribLocation(gl, loc, name); + + if(isVertexAttribArrayEnabled(name)) { + // enable attrib, VBO and pass location/data + gl.glEnableVertexAttribArray(loc); + } + + if( attribute.isVBO() ) { + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, attribute.getVBOName()); + gl.glVertexAttribPointer(attribute); + gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0); + } else { + gl.glVertexAttribPointer(attribute); + } + } + } + + /** + * preserves the attribute location .. (program not linked) + */ + private final void setAllAttributes(final GL2ES2 gl) { + for(final Iterator iter = activeAttribDataMap.values().iterator(); iter.hasNext(); ) { + setAttribute(gl, iter.next()); + } + } + + // + // Shader Uniform handling + // + + /** + * Gets the cached location of the shader uniform. + * + * @return -1 if there is no such uniform available, + * otherwise >= 0 + */ + public final int getCachedUniformLocation(final String name) { + final Integer idx = activeUniformLocationMap.get(name); + return (null!=idx)?idx.intValue():-1; + } + + public boolean ownsUniform(final GLUniformData uniform) { + return managedUniforms.contains(uniform); + } + + /** + * Gets the location of a shader uniform with given name.
+ * Uses either the cached value {@link #getCachedUniformLocation(String)} if valid, + * or the GLSL queried via {@link GL2ES2#glGetUniformLocation(int, String)}.
+ * The location will be cached. + *

+ * The current shader program ({@link #attachShaderProgram(GL2ES2, ShaderProgram)}) + * must be in use ({@link #useProgram(GL2ES2, boolean) }) !

+ * + * @return -1 if there is no such attribute available, + * otherwise >= 0 + + + */ + public final int getUniformLocation(final GL2ES2 gl, final String name) { + if(!shaderProgram.inUse()) throw new GLException("Program is not in use"); + int location = getCachedUniformLocation(name); + if(0>location) { + if(!shaderProgram.linked()) throw new GLException("Program is not linked"); + location = gl.glGetUniformLocation(shaderProgram.program(), name); + if(0<=location) { + activeUniformLocationMap.put(name, Integer.valueOf(location)); + } else if(verbose) { + System.err.println("ShaderState: glUniform failed, no location for: "+name+", index: "+location); + if(DEBUG) { + ExceptionUtils.dumpStack(System.err); + } + } + } + return location; + } + + /** + * Validates and returns the location of a shader uniform.
+ * Uses either the cached value {@link #getCachedUniformLocation(String)} if valid, + * or the GLSL queried via {@link GL2ES2#glGetUniformLocation(int, String)}.
+ * The location will be cached and set in the + * {@link GLUniformData} object. + *

+ * The current shader program ({@link #attachShaderProgram(GL2ES2, ShaderProgram)}) + * must be in use ({@link #useProgram(GL2ES2, boolean) }) !

+ * + * @return -1 if there is no such attribute available, + * otherwise >= 0 + + + */ + public int getUniformLocation(final GL2ES2 gl, final GLUniformData data) { + if(!shaderProgram.inUse()) throw new GLException("Program is not in use"); + final String name = data.getName(); + int location = getCachedUniformLocation(name); + if(0<=location) { + data.setLocation(location); + } else { + if(!shaderProgram.linked()) throw new GLException("Program is not linked"); + location = data.setLocation(gl, shaderProgram.program()); + if(0<=location) { + activeUniformLocationMap.put(name, Integer.valueOf(location)); + } else if(verbose) { + System.err.println("ShaderState: glUniform failed, no location for: "+name+", index: "+location); + if(DEBUG) { + ExceptionUtils.dumpStack(System.err); + } + } + } + activeUniformDataMap.put(name, data); + return location; + } + + /** + * Set the uniform data, if it's location is valid, i.e. ≥ 0. + *

+ * This method uses the {@link GLUniformData}'s location if valid, i.e. ≥ 0.
+ * If data's location is invalid, it will be retrieved via {@link #getUniformLocation(GL2ES2, GLUniformData)}, + * set and cached in this state. + *

+ * + * @return false, if the location could not be determined, otherwise true + */ + public boolean uniform(final GL2ES2 gl, final GLUniformData data) { + if(!shaderProgram.inUse()) throw new GLException("Program is not in use"); + int location = data.getLocation(); + if(0>location) { + location = getUniformLocation(gl, data); + } + if(0<=location) { + // only pass the data, if the uniform exists in the current shader + if(DEBUG) { + System.err.println("ShaderState: glUniform: "+data); + } + gl.glUniform(data); + return true; + } + return false; + } + + /** + * Get the uniform data, previously set. + * + * @return the GLUniformData object, null if not previously set. + */ + public GLUniformData getUniform(final String name) { + return activeUniformDataMap.get(name); + } + + /** + * Releases all mapped uniform data + * and loses all indices + */ + public void releaseAllUniforms(final GL2ES2 gl) { + activeUniformDataMap.clear(); + activeUniformLocationMap.clear(); + managedUniforms.clear(); + } + + /** + * Reset all previously mapped uniform data + *

+ * Uniform data and location is bound to the program, + * hence both are updated. + *

+ *

+ * Note: Such update could only be prevented, + * if tracking a uniform/program dirty flag. + *

+ * + + * + * @see #attachShaderProgram(GL2ES2, ShaderProgram) + */ + private final void resetAllUniforms(final GL2ES2 gl) { + if(!shaderProgram.inUse()) throw new GLException("Program is not in use"); + activeUniformLocationMap.clear(); + for(final Iterator iter = managedUniforms.iterator(); iter.hasNext(); ) { + iter.next().setLocation(-1); + } + for(final Iterator iter = activeUniformDataMap.values().iterator(); iter.hasNext(); ) { + final GLUniformData data = iter.next(); + final int loc = data.setLocation(gl, shaderProgram.program()); + if( 0 <= loc ) { + // only pass the data, if the uniform exists in the current shader + activeUniformLocationMap.put(data.getName(), Integer.valueOf(loc)); + if(DEBUG) { + System.err.println("ShaderState: resetAllUniforms: "+data); + } + gl.glUniform(data); + } + } + } + + public StringBuilder toString(StringBuilder sb, final boolean alsoUnlocated) { + if(null==sb) { + sb = new StringBuilder(); + } + + sb.append("ShaderState[ "); + + sb.append(Platform.getNewline()).append(" "); + if(null != shaderProgram) { + shaderProgram.toString(sb); + } else { + sb.append("ShaderProgram: null"); + } + sb.append(Platform.getNewline()).append(" enabledAttributes ["); + { + final Iterator names = activedAttribEnabledMap.keySet().iterator(); + final Iterator values = activedAttribEnabledMap.values().iterator(); + while( names.hasNext() ) { + sb.append(Platform.getNewline()).append(" ").append(names.next()).append(": ").append(values.next()); + } + } + sb.append(Platform.getNewline()).append(" ],").append(" activeAttributes ["); + for(final Iterator iter = activeAttribDataMap.values().iterator(); iter.hasNext(); ) { + final GLArrayData ad = iter.next(); + if( alsoUnlocated || 0 <= ad.getLocation() ) { + sb.append(Platform.getNewline()).append(" ").append(ad); + } + } + sb.append(Platform.getNewline()).append(" ],").append(" managedAttributes ["); + for(final Iterator iter = managedAttributes.iterator(); iter.hasNext(); ) { + final GLArrayData ad = iter.next(); + if( alsoUnlocated || 0 <= ad.getLocation() ) { + sb.append(Platform.getNewline()).append(" ").append(ad); + } + } + sb.append(Platform.getNewline()).append(" ],").append(" activeUniforms ["); + for(final Iterator iter=activeUniformDataMap.values().iterator(); iter.hasNext(); ) { + final GLUniformData ud = iter.next(); + if( alsoUnlocated || 0 <= ud.getLocation() ) { + sb.append(Platform.getNewline()).append(" ").append(ud); + } + } + sb.append(Platform.getNewline()).append(" ],").append(" managedUniforms ["); + for(final Iterator iter = managedUniforms.iterator(); iter.hasNext(); ) { + final GLUniformData ud = iter.next(); + if( alsoUnlocated || 0 <= ud.getLocation() ) { + sb.append(Platform.getNewline()).append(" ").append(ud); + } + } + sb.append(Platform.getNewline()).append(" ]").append(Platform.getNewline()).append("]"); + return sb; + } + + @Override + public String toString() { + return toString(null, DEBUG).toString(); + } + + private boolean verbose = DEBUG; + private ShaderProgram shaderProgram=null; + + private final HashMap activedAttribEnabledMap = new HashMap(); + private final HashMap activeAttribLocationMap = new HashMap(); + private final HashMap activeAttribDataMap = new HashMap(); + private final ArrayList managedAttributes = new ArrayList(); + + private final HashMap activeUniformLocationMap = new HashMap(); + private final HashMap activeUniformDataMap = new HashMap(); + private final ArrayList managedUniforms = new ArrayList(); + + private final HashMap attachedObjectsByString = new HashMap(); + private boolean resetAllShaderData = false; +} + diff --git a/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderUtil.java b/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderUtil.java new file mode 100644 index 00000000..ea0cfd77 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/glsl/ShaderUtil.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2009 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + */ + +package net.opengrabeso.opengl.util.glsl; + +import java.io.PrintStream; +import java.nio.*; +import java.util.*; + +import com.jogamp.opengl.*; + +import com.jogamp.common.nio.Buffers; +import com.jogamp.opengl.GLExtensions; + +public class ShaderUtil { + public static String getShaderInfoLog(final GL _gl, final int shaderObj) { + final GL2ES2 gl = _gl.getGL2ES2(); + final int[] infoLogLength=new int[1]; + gl.glGetShaderiv(shaderObj, GL2ES2.GL_INFO_LOG_LENGTH, infoLogLength, 0); + + if(infoLogLength[0]==0) { + return "(no info log)"; + } + final int[] charsWritten=new int[1]; + final byte[] infoLogBytes = new byte[infoLogLength[0]]; + gl.glGetShaderInfoLog(shaderObj, infoLogLength[0], charsWritten, 0, infoLogBytes, 0); + + return new String(infoLogBytes, 0, charsWritten[0]); + } + + public static String getProgramInfoLog(final GL _gl, final int programObj) { + final GL2ES2 gl = _gl.getGL2ES2(); + final int[] infoLogLength=new int[1]; + gl.glGetProgramiv(programObj, GL2ES2.GL_INFO_LOG_LENGTH, infoLogLength, 0); + + if(infoLogLength[0]==0) { + return "(no info log)"; + } + final int[] charsWritten=new int[1]; + final byte[] infoLogBytes = new byte[infoLogLength[0]]; + gl.glGetProgramInfoLog(programObj, infoLogLength[0], charsWritten, 0, infoLogBytes, 0); + + return new String(infoLogBytes, 0, charsWritten[0]); + } + + public static boolean isShaderStatusValid(final GL _gl, final int shaderObj, final int name, final PrintStream verboseOut) { + final GL2ES2 gl = _gl.getGL2ES2(); + final int[] ires = new int[1]; + gl.glGetShaderiv(shaderObj, name, ires, 0); + + final boolean res = ires[0]==1; + if(!res && null!=verboseOut) { + verboseOut.println("Shader status invalid: "+ getShaderInfoLog(gl, shaderObj)); + } + return res; + } + + public static boolean isShaderStatusValid(final GL _gl, final IntBuffer shaders, final int name, final PrintStream verboseOut) { + boolean res = true; + for (int i = shaders.position(); i < shaders.limit(); i++) { + res = isShaderStatusValid(_gl, shaders.get(i), name, verboseOut) && res; + } + return res; + } + + public static boolean isProgramStatusValid(final GL _gl, final int programObj, final int name) { + final GL2ES2 gl = _gl.getGL2ES2(); + final int[] ires = new int[1]; + gl.glGetProgramiv(programObj, name, ires, 0); + + return ires[0]==1; + } + + public static boolean isProgramLinkStatusValid(final GL _gl, final int programObj, final PrintStream verboseOut) { + final GL2ES2 gl = _gl.getGL2ES2(); + if(!gl.glIsProgram(programObj)) { + if(null!=verboseOut) { + verboseOut.println("Program name invalid: "+programObj); + } + return false; + } + if(!isProgramStatusValid(gl, programObj, GL2ES2.GL_LINK_STATUS)) { + if(null!=verboseOut) { + verboseOut.println("Program link failed: "+programObj+"\n\t"+ getProgramInfoLog(gl, programObj)); + } + return false; + } + return true; + } + + /** + * Performs {@link GL2ES2#glValidateProgram(int)} + *

+ * One shall only call this method while debugging and only if all required + * resources by the shader are set. + *

+ *

+ * Note: It is possible that a working shader program will fail validation. + * This has been experienced on NVidia APX2500 and Tegra2. + *

+ * @see GL2ES2#glValidateProgram(int) + **/ + public static boolean isProgramExecStatusValid(final GL _gl, final int programObj, final PrintStream verboseOut) { + final GL2ES2 gl = _gl.getGL2ES2(); + gl.glValidateProgram(programObj); + if(!isProgramStatusValid(gl, programObj, GL2ES2.GL_VALIDATE_STATUS)) { + if(null!=verboseOut) { + verboseOut.println("Program validation failed: "+programObj+"\n\t"+ getProgramInfoLog(gl, programObj)); + } + return false; + } + return true; + } + + public static void createShader(final GL _gl, final int type, final IntBuffer shaders) { + final GL2ES2 gl = _gl.getGL2ES2(); + for (int i = shaders.position(); i < shaders.limit(); i++) { + shaders.put(i, gl.glCreateShader(type)); + } + } + + /** + * If supported, queries the natively supported shader binary formats using + * {@link GL2ES2#GL_NUM_SHADER_BINARY_FORMATS} and {@link GL2ES2#GL_SHADER_BINARY_FORMATS} + * via {@link GL2ES2#glGetIntegerv(int, int[], int)}. + */ + public static Set getShaderBinaryFormats(final GL _gl) { + return new HashSet(); + } + + /** Returns true if a shader compiler is available, otherwise false. */ + public static boolean isShaderCompilerAvailable(final GL _gl) { + return false; + } + + public static void shaderSource(final GL _gl, final int shader, final CharSequence[] source) + { + final GL2ES2 gl = _gl.getGL2ES2(); + if(!isShaderCompilerAvailable(_gl)) { + throw new GLException("No compiler is available"); + } + + final int count = (null!=source)?source.length:0; + if(count==0) { + throw new GLException("No sources specified"); + } + + final IntBuffer lengths = Buffers.newDirectIntBuffer(count); + for(int i=0; i=0; i--) { + final CharSequence csq = source[i]; + if(csq instanceof String) { + // if ShaderCode.create(.. mutableStringBuilder == false ) + tmp[i] = (String) csq; + } else { + // if ShaderCode.create(.. mutableStringBuilder == true ) + tmp[i] = source[i].toString(); + } + } + gl.glShaderSource(shader, count, tmp, lengths); + } + } + + public static void shaderSource(final GL _gl, final IntBuffer shaders, final CharSequence[][] sources) + { + final int sourceNum = (null!=sources)?sources.length:0; + final int shaderNum = (null!=shaders)?shaders.remaining():0; + if(shaderNum<=0 || sourceNum<=0 || shaderNum!=sourceNum) { + throw new GLException("Invalid number of shaders and/or sources: shaders="+ + shaderNum+", sources="+sourceNum); + } + for(int i=0; i=binLength) { + throw new GLException("Empty shader binary (remaining == 0)"); + } + gl.glShaderBinary(shaderNum, shaders, binFormat, bin, binLength); + } + + public static void compileShader(final GL _gl, final IntBuffer shaders) + { + final GL2ES2 gl = _gl.getGL2ES2(); + for (int i = shaders.position(); i < shaders.limit(); i++) { + gl.glCompileShader(shaders.get(i)); + } + } + + public static void attachShader(final GL _gl, final int program, final IntBuffer shaders) + { + final GL2ES2 gl = _gl.getGL2ES2(); + for (int i = shaders.position(); i < shaders.limit(); i++) { + gl.glAttachShader(program, shaders.get(i)); + } + } + + public static void detachShader(final GL _gl, final int program, final IntBuffer shaders) + { + final GL2ES2 gl = _gl.getGL2ES2(); + for (int i = shaders.position(); i < shaders.limit(); i++) { + gl.glDetachShader(program, shaders.get(i)); + } + } + + public static void deleteShader(final GL _gl, final IntBuffer shaders) + { + final GL2ES2 gl = _gl.getGL2ES2(); + for (int i = shaders.position(); i < shaders.limit(); i++) { + gl.glDeleteShader(shaders.get(i)); + + } + } + + public static boolean createAndLoadShader(final GL _gl, final IntBuffer shader, final int shaderType, + final int binFormat, final Buffer bin, + final PrintStream verboseOut) + { + final GL2ES2 gl = _gl.getGL2ES2(); + int err = gl.glGetError(); // flush previous errors .. + if(err!=GL.GL_NO_ERROR && null!=verboseOut) { + verboseOut.println("createAndLoadShader: Pre GL Error: 0x"+Integer.toHexString(err)); + } + + createShader(gl, shaderType, shader); + err = gl.glGetError(); + if(err!=GL.GL_NO_ERROR) { + throw new GLException("createAndLoadShader: CreateShader failed, GL Error: 0x"+Integer.toHexString(err)); + } + + shaderBinary(gl, shader, binFormat, bin); + + err = gl.glGetError(); + if(err!=GL.GL_NO_ERROR && null!=verboseOut) { + verboseOut.println("createAndLoadShader: ShaderBinary failed, GL Error: 0x"+Integer.toHexString(err)); + } + return err == GL.GL_NO_ERROR; + } + + public static boolean createAndCompileShader(final GL _gl, final IntBuffer shader, final int shaderType, + final CharSequence[][] sources, + final PrintStream verboseOut) + { + final GL2ES2 gl = _gl.getGL2ES2(); + int err = gl.glGetError(); // flush previous errors .. + if(err!=GL.GL_NO_ERROR && null!=verboseOut) { + verboseOut.println("createAndCompileShader: Pre GL Error: 0x"+Integer.toHexString(err)); + } + + createShader(gl, shaderType, shader); + err = gl.glGetError(); + if(err!=GL.GL_NO_ERROR) { + throw new GLException("createAndCompileShader: CreateShader failed, GL Error: 0x"+Integer.toHexString(err)); + } + + shaderSource(gl, shader, sources); + err = gl.glGetError(); + if(err!=GL.GL_NO_ERROR) { + throw new GLException("createAndCompileShader: ShaderSource failed, GL Error: 0x"+Integer.toHexString(err)); + } + + compileShader(gl, shader); + err = gl.glGetError(); + if(err!=GL.GL_NO_ERROR && null!=verboseOut) { + verboseOut.println("createAndCompileShader: CompileShader failed, GL Error: 0x"+Integer.toHexString(err)); + } + + return isShaderStatusValid(gl, shader, GL2ES2.GL_COMPILE_STATUS, verboseOut) && err == GL.GL_NO_ERROR; + } + + private static final String implObjectKey = "com.jogamp.opengl.util.glsl.ShaderUtil" ; + + private static class ProfileInformation { + Boolean shaderCompilerAvailable = null; + Set shaderBinaryFormats = null; + } + + private static ProfileInformation getProfileInformation(final GL gl) { + final GLContext context = gl.getContext(); + context.validateCurrent(); + ProfileInformation data = (ProfileInformation) context.getAttachedObject(implObjectKey); + if (data == null) { + data = new ProfileInformation(); + context.attachObject(implObjectKey, data); + } + return data; + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/glsl/sdk/CompileShader.java b/src/main/java/net/opengrabeso/opengl/util/glsl/sdk/CompileShader.java new file mode 100644 index 00000000..90f706f7 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/glsl/sdk/CompileShader.java @@ -0,0 +1,155 @@ +package net.opengrabeso.opengl.util.glsl.sdk; + +import com.jogamp.common.util.IOUtil; + +import com.jogamp.opengl.*; +import net.opengrabeso.opengl.util.glsl.*; + +import java.io.*; +import java.net.*; + +/** + * Precompiles a shader into a vendor binary format. Input is the + * resource name of the shader, such as + * "com/jogamp/opengl/impl/glsl/fixed/shader/a.fp". + * Output is "com/jogamp/opengl/impl/glsl/fixed/shader/bin/nvidia/a.bfp". + * + * All path and suffixes are determined by the ShaderCode class, + * which ensures runtime compatibility. + * + * @see com.jogamp.opengl.util.glsl.ShaderCode + */ + +public abstract class CompileShader { + + public abstract int getBinaryFormat(); + + public abstract File getSDKCompilerDir(); + + public abstract String getVertexShaderCompiler(); + + public abstract String getFragmentShaderCompiler(); + + public void processOneShader(final String resourceName) + throws IOException, UnsupportedEncodingException, InterruptedException + { + int type = -1; + String outName=null; + int suffixLen = -1; + if(resourceName.endsWith(ShaderCode.getFileSuffix(false, GL2ES2.GL_FRAGMENT_SHADER))) { + suffixLen = 2; + type = GL2ES2.GL_FRAGMENT_SHADER; + } else if(resourceName.endsWith(".frag")) { + suffixLen = 4; + type = GL2ES2.GL_FRAGMENT_SHADER; + } else if(resourceName.endsWith(ShaderCode.getFileSuffix(false, GL2ES2.GL_VERTEX_SHADER))) { + suffixLen = 2; + type = GL2ES2.GL_VERTEX_SHADER; + } else if(resourceName.endsWith(".vert")) { + suffixLen = 4; + type = GL2ES2.GL_VERTEX_SHADER; + } + final String justName = basename(resourceName); + outName = justName.substring(0, justName.length() - suffixLen) + + ShaderCode.getFileSuffix(true, type); + final URL resourceURL = IOUtil.getResource(resourceName, this.getClass().getClassLoader(), null).getURL(); + final String dirName = dirname(resourceURL.getPath()); + + outName = dirName + File.separator + "bin" + File.separator + + ShaderCode.getBinarySubPath(getBinaryFormat()) + File.separator + + outName; + processOneShader(resourceName, outName, type); + } + + public void processOneShader(final String resourceName, final String outName, final int type) + throws IOException, UnsupportedEncodingException, InterruptedException + { + final URL resourceURL = IOUtil.getResource(resourceName, this.getClass().getClassLoader(), null).getURL(); + final String dirName = dirname(resourceURL.getPath()); + + final CharSequence shader = ShaderCode.readShaderSource(null, resourceName, false); + if(null==shader) { + System.err.println("Can't find shader source " + resourceName + " - ignored"); + return; + } + System.err.println("Preprocessing: "+ resourceName+", in dir: "+dirName); + final String justName = basename(resourceName); + String processor; + switch (type) { + case GL2ES2.GL_VERTEX_SHADER: + processor = getVertexShaderCompiler(); + break; + case GL2ES2.GL_FRAGMENT_SHADER: + processor = getFragmentShaderCompiler(); + break; + default: + throw new GLException("Unknown shader type: "+type); + } + final File outputFile = new File(outName); + + // Write shader to a file in java.io.tmpdir + final File tmpDir = new File(dirName+File.separator+"tmp"); + tmpDir.mkdirs(); + final File tmpFile = new File(tmpDir, justName); + final Writer writer = new BufferedWriter(new FileWriter(tmpFile)); + writer.write(shader.toString(), 0, shader.length()); + writer.flush(); + writer.close(); + System.err.println("Preprocessed: "+ tmpFile.getAbsolutePath()); + + final File processorDir = getSDKCompilerDir(); + + System.err.println("SDK: "+ processorDir.getAbsolutePath() + ", compiler: "+processor); + + System.err.println("Output: "+ outputFile.getAbsolutePath()); + + // Run the tool + final Process process = Runtime.getRuntime().exec(new String[] { + processorDir.getAbsolutePath() + File.separator + processor, + tmpFile.getAbsolutePath(), + outputFile.getAbsolutePath() + }); // , null, processorDir); + new IOUtil.StreamMonitor( new InputStream[] { process.getInputStream(), process.getErrorStream() }, System.out, null ); + process.waitFor(); + // Delete the temporary file + // tmpFile.delete(); + } + + protected static String basename(final String path) { + int lastSlash = path.lastIndexOf("/"); + if (lastSlash < 0) { + lastSlash = path.lastIndexOf("\\"); + } + String basename; + if (lastSlash < 0) { + basename = path; + } else { + basename = path.substring(lastSlash + 1); + } + return basename; + } + + protected static String dirname(final String path) { + int lastSlash = path.lastIndexOf("/"); + if (lastSlash < 0) { + lastSlash = path.lastIndexOf("\\"); + } + String dirname; + if (lastSlash < 0) { + dirname = ""; + } else { + dirname = path.substring(0, lastSlash + 1); + } + return dirname; + } + + public void run(final String[] args) { + try { + for (int i = 0; i < args.length; i++) { + processOneShader(args[i]); + } + } catch (final Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/glsl/sdk/CompileShaderNVidia.java b/src/main/java/net/opengrabeso/opengl/util/glsl/sdk/CompileShaderNVidia.java new file mode 100644 index 00000000..73252e65 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/glsl/sdk/CompileShaderNVidia.java @@ -0,0 +1,57 @@ +package net.opengrabeso.opengl.util.glsl.sdk; + +import com.jogamp.opengl.*; + +import java.io.*; + +/** Precompiles a shader into NVidia binary format. Input is the + resource name of the shader, such as + "com/jogamp/opengl/impl/glsl/fixed/shader/a.fp". + Output is "com/jogamp/opengl/impl/glsl/fixed/shader/bin/nvidia/a.bfp". */ + +public class CompileShaderNVidia extends CompileShader { + private static final String NVAPSDK; + + static { + final String nvapSDKProp = System.getProperty("NVAPSDK"); + if (nvapSDKProp != null) { + NVAPSDK = nvapSDKProp; + } else { + NVAPSDK = "C:\\nvap_sdk_0_3_x"; + } + } + + @Override + public int getBinaryFormat() { + return GLES2.GL_NVIDIA_PLATFORM_BINARY_NV; + } + + @Override + public File getSDKCompilerDir() { + File compilerDir = new File( NVAPSDK + File.separator + "tools" + File.separator ); + File compilerFile = new File( compilerDir, getVertexShaderCompiler()); + if(!compilerFile.exists()) { + compilerDir = new File( NVAPSDK ); + compilerFile = new File( compilerDir, getVertexShaderCompiler()); + } + if(!compilerFile.exists()) { + throw new GLException("Can't find compiler: "+getVertexShaderCompiler() + " in : " + + NVAPSDK+", "+NVAPSDK + File.separator + "tools"); + } + return compilerDir; + } + + @Override + public String getVertexShaderCompiler() { + return "glslv.bat"; + } + + @Override + public String getFragmentShaderCompiler() { + return "glslf.bat"; + } + + public static void main(final String[] args) { + new CompileShaderNVidia().run(args); + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/packrect/BackingStoreManager.java b/src/main/java/net/opengrabeso/opengl/util/packrect/BackingStoreManager.java new file mode 100644 index 00000000..1cd96f71 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/packrect/BackingStoreManager.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + * + * Sun gratefully acknowledges that this software was originally authored + * and developed by Kenneth Bradley Russell and Christopher John Kline. + */ + +package net.opengrabeso.opengl.util.packrect; + +/** This interface must be implemented by the end user and is called + in response to events like addition of rectangles into the + RectanglePacker. It is used both when a full re-layout must be + done as well as when the data in the backing store must be copied + to a new one. */ + +public interface BackingStoreManager { + public Object allocateBackingStore(int w, int h); + public void deleteBackingStore(Object backingStore); + + /** Indication whether this BackingStoreManager supports compaction; + in other words, the allocation of a new backing store and + movement of the contents of the backing store from the old to + the new one. If it does not, then RectanglePacker.add() may + throw an exception if additionFailed() can not make enough room + available. If an implementation returns false, this also implies + that the backing store can not grow, so that preExpand() will + never be called. */ + public boolean canCompact(); + + /** Notification that expansion of the backing store is about to be + done due to addition of the given rectangle. Gives the manager a + chance to do some compaction and potentially remove old entries + from the backing store, if it acts like a least-recently-used + cache. This method receives as argument the number of attempts + so far to add the given rectangle. Manager should return true if + the RectanglePacker should retry the addition (which may result + in this method being called again, with an increased attempt + number) or false if the RectanglePacker should just expand the + backing store. The caller should not call RectanglePacker.add() + in its preExpand() method. */ + public boolean preExpand(Rect cause, int attemptNumber); + + /** Notification that addition of the given Rect failed because a + maximum size was set in the RectanglePacker and the backing + store could not be expanded, or because compaction (and, + therefore, implicitly expansion) was not supported. Should + return false if the manager can do nothing more to handle the + failed addition, which will cause a RuntimeException to be + thrown from the RectanglePacker. */ + public boolean additionFailed(Rect cause, int attemptNumber); + + /** Notification that movement is starting. */ + public void beginMovement(Object oldBackingStore, Object newBackingStore); + + /** Tells the manager to move the contents of the given rect from + the old location on the old backing store to the new location on + the new backing store. The backing stores can be identical in + the case of compacting the existing backing store instead of + reallocating it. */ + public void move(Object oldBackingStore, + Rect oldLocation, + Object newBackingStore, + Rect newLocation); + + /** Notification that movement is ending. */ + public void endMovement(Object oldBackingStore, Object newBackingStore); +} diff --git a/src/main/java/net/opengrabeso/opengl/util/packrect/Level.java b/src/main/java/net/opengrabeso/opengl/util/packrect/Level.java new file mode 100644 index 00000000..71fd0729 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/packrect/Level.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + * + * Sun gratefully acknowledges that this software was originally authored + * and developed by Kenneth Bradley Russell and Christopher John Kline. + */ + +package net.opengrabeso.opengl.util.packrect; + +import java.util.*; + +public class Level { + private final int width; + private int height; + private final int yPos; + private final LevelSet holder; + + private final List rects = new ArrayList(); + private List freeList; + private int nextAddX; + + static class RectXComparator implements Comparator { + @Override + public int compare(final Rect r1, final Rect r2) { + return r1.x() - r2.x(); + } + + @Override + public boolean equals(final Object obj) { + return this == obj; + } + } + private static final Comparator rectXComparator = new RectXComparator(); + + public Level(final int width, final int height, final int yPos, final LevelSet holder) { + this.width = width; + this.height = height; + this.yPos = yPos; + this.holder = holder; + } + + public int w() { return width; } + public int h() { return height; } + public int yPos() { return yPos; } + + /** Tries to add the given rectangle to this level only allowing + non-disruptive changes like trivial expansion of the last level + in the RectanglePacker and allocation from the free list. More + disruptive changes like compaction of the level must be + requested explicitly. */ + public boolean add(final Rect rect) { + if (rect.h() > height) { + // See whether it's worth trying to expand vertically + if (nextAddX + rect.w() > width) { + return false; + } + + // See whether we're the last level and can expand + if (!holder.canExpand(this, rect.h())) { + return false; + } + + // Trivially expand and try the allocation + holder.expand(this, height, rect.h()); + height = rect.h(); + } + + // See whether we can add at the end + if (nextAddX + rect.w() <= width) { + rect.setPosition(nextAddX, yPos); + rects.add(rect); + nextAddX += rect.w(); + return true; + } + + // See whether we can add from the free list + if (freeList != null) { + Rect candidate = null; + for (final Iterator iter = freeList.iterator(); iter.hasNext(); ) { + final Rect cur = iter.next(); + if (cur.canContain(rect)) { + candidate = cur; + break; + } + } + + if (candidate != null) { + // Remove the candidate from the free list + freeList.remove(candidate); + // Set up and add the real rect + rect.setPosition(candidate.x(), candidate.y()); + rects.add(rect); + // Re-add any remaining free space + if (candidate.w() > rect.w()) { + candidate.setPosition(candidate.x() + rect.w(), candidate.y()); + candidate.setSize(candidate.w() - rect.w(), height); + freeList.add(candidate); + } + + coalesceFreeList(); + + return true; + } + } + + return false; + } + + /** Removes the given Rect from this Level. */ + public boolean remove(final Rect rect) { + if (!rects.remove(rect)) + return false; + + // If this is the rightmost rectangle, instead of adding its space + // to the free list, we can just decrease the nextAddX + if (rect.maxX() + 1 == nextAddX) { + nextAddX -= rect.w(); + } else { + if (freeList == null) { + freeList = new ArrayList(); + } + freeList.add(new Rect(rect.x(), rect.y(), rect.w(), height, null)); + coalesceFreeList(); + } + + return true; + } + + /** Indicates whether this Level contains no rectangles. */ + public boolean isEmpty() { + return rects.isEmpty(); + } + + /** Indicates whether this Level could satisfy an allocation request + if it were compacted. */ + public boolean couldAllocateIfCompacted(final Rect rect) { + if (rect.h() > height) + return false; + if (freeList == null) + return false; + int freeListWidth = 0; + for (final Iterator iter = freeList.iterator(); iter.hasNext(); ) { + final Rect cur = iter.next(); + freeListWidth += cur.w(); + } + // Add on the remaining space at the end + freeListWidth += (width - nextAddX); + return (freeListWidth >= rect.w()); + } + + public void compact(final Object backingStore, final BackingStoreManager manager) { + Collections.sort(rects, rectXComparator); + int nextCompactionDest = 0; + manager.beginMovement(backingStore, backingStore); + for (final Iterator iter = rects.iterator(); iter.hasNext(); ) { + final Rect cur = iter.next(); + if (cur.x() != nextCompactionDest) { + manager.move(backingStore, cur, + backingStore, new Rect(nextCompactionDest, cur.y(), cur.w(), cur.h(), null)); + cur.setPosition(nextCompactionDest, cur.y()); + } + nextCompactionDest += cur.w(); + } + nextAddX = nextCompactionDest; + freeList.clear(); + manager.endMovement(backingStore, backingStore); + } + + public Iterator iterator() { + return rects.iterator(); + } + + /** Visits all Rects contained in this Level. */ + public void visit(final RectVisitor visitor) { + for (final Iterator iter = rects.iterator(); iter.hasNext(); ) { + final Rect rect = iter.next(); + visitor.visit(rect); + } + } + + /** Updates the references to the Rect objects in this Level with + the "next locations" of those Rects. This is actually used to + update the new Rects in a newly laid-out LevelSet with the + original Rects. */ + public void updateRectangleReferences() { + for (int i = 0; i < rects.size(); i++) { + final Rect cur = rects.get(i); + final Rect next = cur.getNextLocation(); + next.setPosition(cur.x(), cur.y()); + if (cur.w() != next.w() || cur.h() != next.h()) + throw new RuntimeException("Unexpected disparity in rectangle sizes during updateRectangleReferences"); + rects.set(i, next); + } + } + + private void coalesceFreeList() { + if (freeList == null) + return; + if (freeList.isEmpty()) + return; + + // Try to coalesce adjacent free blocks in the free list + Collections.sort(freeList, rectXComparator); + int i = 0; + while (i < freeList.size() - 1) { + final Rect r1 = freeList.get(i); + final Rect r2 = freeList.get(i+1); + if (r1.maxX() + 1 == r2.x()) { + // Coalesce r1 and r2 into one block + freeList.remove(i+1); + r1.setSize(r1.w() + r2.w(), r1.h()); + } else { + ++i; + } + } + // See whether the last block bumps up against the addition point + final Rect last = freeList.get(freeList.size() - 1); + if (last.maxX() + 1 == nextAddX) { + nextAddX -= last.w(); + freeList.remove(freeList.size() - 1); + } + if (freeList.isEmpty()) { + freeList = null; + } + } + + //---------------------------------------------------------------------- + // Debugging functionality + // + + public void dumpFreeSpace() { + int freeListWidth = 0; + for (final Iterator iter = freeList.iterator(); iter.hasNext(); ) { + final Rect cur = iter.next(); + System.err.println(" Free rectangle at " + cur); + freeListWidth += cur.w(); + } + // Add on the remaining space at the end + System.err.println(" Remaining free space " + (width - nextAddX)); + freeListWidth += (width - nextAddX); + System.err.println(" Total free space " + freeListWidth); + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/packrect/LevelSet.java b/src/main/java/net/opengrabeso/opengl/util/packrect/LevelSet.java new file mode 100644 index 00000000..3b1faa4d --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/packrect/LevelSet.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + * + * Sun gratefully acknowledges that this software was originally authored + * and developed by Kenneth Bradley Russell and Christopher John Kline. + */ + +package net.opengrabeso.opengl.util.packrect; + +import java.util.*; + +/** Manages a list of Levels; this is the core data structure + contained within the RectanglePacker and encompasses the storage + algorithm for the contained Rects. */ + +public class LevelSet { + // Maintained in sorted order by increasing Y coordinate + private final List levels = new ArrayList(); + private int nextAddY; + private final int w; + private int h; + + /** A LevelSet manages all of the backing store for a region of a + specified width and height. */ + public LevelSet(final int w, final int h) { + this.w = w; + this.h = h; + } + + public int w() { return w; } + public int h() { return h; } + + /** Returns true if the given rectangle was successfully added to + the LevelSet given its current dimensions, false if not. Caller + is responsible for performing compaction, expansion, etc. as a + consequence. */ + public boolean add(final Rect rect) { + if (rect.w() > w) + return false; + + // Go in reverse order through the levels seeing whether we can + // trivially satisfy the allocation request + for (int i = levels.size() - 1; i >= 0; --i) { + final Level level = levels.get(i); + if (level.add(rect)) + return true; + } + + // See whether compaction could satisfy this allocation. This + // increases the computational complexity of the addition process, + // but prevents us from expanding unnecessarily. + for (int i = levels.size() - 1; i >= 0; --i) { + final Level level = levels.get(i); + if (level.couldAllocateIfCompacted(rect)) + return false; + } + + // OK, we need to either add a new Level or expand the backing + // store. Try to add a new Level. + if (nextAddY + rect.h() > h) + return false; + + final Level newLevel = new Level(w, rect.h(), nextAddY, this); + levels.add(newLevel); + nextAddY += rect.h(); + final boolean res = newLevel.add(rect); + if (!res) + throw new RuntimeException("Unexpected failure in addition to new Level"); + return true; + } + + /** Removes the given Rect from this LevelSet. */ + public boolean remove(final Rect rect) { + for (int i = levels.size() - 1; i >= 0; --i) { + final Level level = levels.get(i); + if (level.remove(rect)) + return true; + } + + return false; + } + + /** Allocates the given Rectangle, performing compaction of a Level + if necessary. This is the correct fallback path to {@link + #add(Rect)} above. Returns true if allocated successfully, false + otherwise (indicating the need to expand the backing store). */ + public boolean compactAndAdd(final Rect rect, + final Object backingStore, + final BackingStoreManager manager) { + for (int i = levels.size() - 1; i >= 0; --i) { + final Level level = levels.get(i); + if (level.couldAllocateIfCompacted(rect)) { + level.compact(backingStore, manager); + final boolean res = level.add(rect); + if (!res) + throw new RuntimeException("Unexpected failure to add after compaction"); + return true; + } + } + + return false; + } + + /** Indicates whether it's legal to trivially increase the height of + the given Level. This is only possible if it's the last Level + added and there's enough room in the backing store. */ + public boolean canExpand(final Level level, final int height) { + if (levels.isEmpty()) + return false; // Should not happen + if (levels.get(levels.size() - 1) == level && + (h - nextAddY >= height - level.h())) + return true; + return false; + } + + public void expand(final Level level, final int oldHeight, final int newHeight) { + nextAddY += (newHeight - oldHeight); + } + + /** Gets the used height of the levels in this LevelSet. */ + public int getUsedHeight() { + return nextAddY; + } + + /** Sets the height of this LevelSet. It is only legal to reduce the + height to greater than or equal to the currently used height. */ + public void setHeight(final int height) throws IllegalArgumentException { + if (height < getUsedHeight()) { + throw new IllegalArgumentException("May not reduce height below currently used height"); + } + h = height; + } + + /** Returns the vertical fragmentation ratio of this LevelSet. This + is defined as the ratio of the sum of the heights of all + completely empty Levels divided by the overall used height of + the LevelSet. A high vertical fragmentation ratio indicates that + it may be profitable to perform a compaction. */ + public float verticalFragmentationRatio() { + int freeHeight = 0; + final int usedHeight = getUsedHeight(); + if (usedHeight == 0) + return 0.0f; + for (final Iterator iter = iterator(); iter.hasNext(); ) { + final Level level = iter.next(); + if (level.isEmpty()) { + freeHeight += level.h(); + } + } + return (float) freeHeight / (float) usedHeight; + } + + public Iterator iterator() { + return levels.iterator(); + } + + /** Visits all Rects contained in this LevelSet. */ + public void visit(final RectVisitor visitor) { + for (final Iterator iter = levels.iterator(); iter.hasNext(); ) { + final Level level = iter.next(); + level.visit(visitor); + } + } + + /** Updates the references to the Rect objects in this LevelSet with + the "next locations" of those Rects. This is actually used to + update the new Rects in a newly laid-out LevelSet with the + original Rects. */ + public void updateRectangleReferences() { + for (final Iterator iter = levels.iterator(); iter.hasNext(); ) { + final Level level = iter.next(); + level.updateRectangleReferences(); + } + } + + /** Clears out all Levels stored in this LevelSet. */ + public void clear() { + levels.clear(); + nextAddY = 0; + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/packrect/Rect.java b/src/main/java/net/opengrabeso/opengl/util/packrect/Rect.java new file mode 100644 index 00000000..456b0fd7 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/packrect/Rect.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + * + * Sun gratefully acknowledges that this software was originally authored + * and developed by Kenneth Bradley Russell and Christopher John Kline. + */ + +package net.opengrabeso.opengl.util.packrect; + +/** Represents a rectangular region on the backing store. The edges of + the rectangle are the infinitely thin region between adjacent + pixels on the screen. The origin of the rectangle is its + upper-left corner. It is inclusive of the pixels on the top and + left edges and exclusive of the pixels on the bottom and right + edges. For example, a rect at position (0, 0) and of size (1, 1) + would include only the pixel at (0, 0).

+ + Negative coordinates and sizes are not supported, since they make + no sense in the context of the packer, which deals only with + positively sized regions.

+ + This class contains a user data field for efficient hookup to + external data structures as well as enough other hooks to + efficiently plug into the rectangle packer. */ + +public class Rect { + private int x; + private int y; + private int w; + private int h; + + // The level we're currently installed in in the parent + // RectanglePacker, or null if not hooked in to the table yet + private Level level; + + // The user's object this rectangle represents. + private Object userData; + + // Used transiently during re-layout of the backing store (when + // there is no room left due either to fragmentation or just being + // out of space) + private Rect nextLocation; + + public Rect() { + this(null); + } + + public Rect(final Object userData) { + this(0, 0, 0, 0, userData); + } + + public Rect(final int x, final int y, final int w, final int h, final Object userData) { + setPosition(x, y); + setSize(w, h); + setUserData(userData); + } + + public int x() { return x; } + public int y() { return y; } + public int w() { return w; } + public int h() { return h; } + public Object getUserData() { return userData; } + public Rect getNextLocation() { return nextLocation; } + + public void setPosition(final int x, final int y) { + if (x < 0) + throw new IllegalArgumentException("Negative x"); + if (y < 0) + throw new IllegalArgumentException("Negative y"); + this.x = x; + this.y = y; + } + + public void setSize(final int w, final int h) throws IllegalArgumentException { + if (w < 0) + throw new IllegalArgumentException("Negative width"); + if (h < 0) + throw new IllegalArgumentException("Negative height"); + this.w = w; + this.h = h; + } + + public void setUserData(final Object obj) { userData = obj; } + public void setNextLocation(final Rect nextLocation) { this.nextLocation = nextLocation; } + + // Helpers for computations. + + /** Returns the maximum x-coordinate contained within this + rectangle. Note that this returns a different result than Java + 2D's rectangles; for a rectangle of position (0, 0) and size (1, + 1) this will return 0, not 1. Returns -1 if the width of this + rectangle is 0. */ + public int maxX() { + if (w() < 1) + return -1; + return x() + w() - 1; + } + + /** Returns the maximum y-coordinate contained within this + rectangle. Note that this returns a different result than Java + 2D's rectangles; for a rectangle of position (0, 0) and size (1, + 1) this will return 0, not 1. Returns -1 if the height of this + rectangle is 0. */ + public int maxY() { + if (h() < 1) + return -1; + return y() + h() - 1; + } + + public boolean canContain(final Rect other) { + return (w() >= other.w() && + h() >= other.h()); + } + + @Override + public String toString() { + return "[Rect x: " + x() + " y: " + y() + " w: " + w() + " h: " + h() + "]"; + } + + // Unclear whether it's a good idea to override hashCode and equals + // for these objects + /* + public boolean equals(Object other) { + if (other == null || (!(other instanceof Rect))) { + return false; + } + + Rect r = (Rect) other; + return (this.x() == r.x() && + this.y() == r.y() && + this.w() == r.w() && + this.h() == r.h()); + } + + public int hashCode() { + return (x + y * 13 + w * 17 + h * 23); + } + */ +} diff --git a/src/main/java/net/opengrabeso/opengl/util/packrect/RectVisitor.java b/src/main/java/net/opengrabeso/opengl/util/packrect/RectVisitor.java new file mode 100644 index 00000000..25001879 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/packrect/RectVisitor.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + * + * Sun gratefully acknowledges that this software was originally authored + * and developed by Kenneth Bradley Russell and Christopher John Kline. + */ + +package net.opengrabeso.opengl.util.packrect; + +/** Iteration construct without exposing the internals of the + RectanglePacker and without implementing a complex Iterator. */ + +public interface RectVisitor { + public void visit(Rect rect); +} diff --git a/src/main/java/net/opengrabeso/opengl/util/packrect/RectanglePacker.java b/src/main/java/net/opengrabeso/opengl/util/packrect/RectanglePacker.java new file mode 100644 index 00000000..9ae58dba --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/packrect/RectanglePacker.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + * + * Sun gratefully acknowledges that this software was originally authored + * and developed by Kenneth Bradley Russell and Christopher John Kline. + */ + +package net.opengrabeso.opengl.util.packrect; + +import java.util.*; + +/** Packs rectangles supplied by the user (typically representing + image regions) into a larger backing store rectangle (typically + representing a large texture). Supports automatic compaction of + the space on the backing store, and automatic expansion of the + backing store, when necessary. */ + +public class RectanglePacker { + private final BackingStoreManager manager; + private Object backingStore; + private LevelSet levels; + private static final float EXPANSION_FACTOR = 0.5f; + private static final float SHRINK_FACTOR = 0.3f; + + private final int initialWidth; + private final int initialHeight; + + private int maxWidth = -1; + private int maxHeight = -1; + + static class RectHComparator implements Comparator { + @Override + public int compare(final Rect r1, final Rect r2) { + return r2.h() - r1.h(); + } + + @Override + public boolean equals(final Object obj) { + return this == obj; + } + } + private static final Comparator rectHComparator = new RectHComparator(); + + public RectanglePacker(final BackingStoreManager manager, + final int initialWidth, + final int initialHeight) { + this.manager = manager; + levels = new LevelSet(initialWidth, initialHeight); + this.initialWidth = initialWidth; + this.initialHeight = initialHeight; + } + + public Object getBackingStore() { + if (backingStore == null) { + backingStore = manager.allocateBackingStore(levels.w(), levels.h()); + } + + return backingStore; + } + + /** Sets up a maximum width and height for the backing store. These + are optional and if not specified the backing store will grow as + necessary. Setting up a maximum width and height introduces the + possibility that additions will fail; these are handled with the + BackingStoreManager's allocationFailed notification. */ + public void setMaxSize(final int maxWidth, final int maxHeight) { + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + } + + /** Decides upon an (x, y) position for the given rectangle (leaving + its width and height unchanged) and places it on the backing + store. May provoke re-layout of other Rects already added. If + the BackingStoreManager does not support compaction, and {@link + BackingStoreManager#preExpand BackingStoreManager.preExpand} + does not clear enough space for the incoming rectangle, then + this method will throw a RuntimeException. */ + public void add(final Rect rect) throws RuntimeException { + // Allocate backing store if we don't have any yet + if (backingStore == null) + backingStore = manager.allocateBackingStore(levels.w(), levels.h()); + + int attemptNumber = 0; + boolean tryAgain = false; + + do { + // Try to allocate + if (levels.add(rect)) + return; + + if (manager.canCompact()) { + // Try to allocate with horizontal compaction + if (levels.compactAndAdd(rect, backingStore, manager)) + return; + // Let the manager have a chance at potentially evicting some entries + tryAgain = manager.preExpand(rect, attemptNumber++); + } else { + tryAgain = manager.additionFailed(rect, attemptNumber++); + } + } while (tryAgain); + + if (!manager.canCompact()) { + throw new RuntimeException("BackingStoreManager does not support compaction or expansion, and didn't clear space for new rectangle"); + } + + compactImpl(rect); + + // Retry the addition of the incoming rectangle + add(rect); + // Done + } + + /** Removes the given rectangle from this RectanglePacker. */ + public void remove(final Rect rect) { + levels.remove(rect); + } + + /** Visits all Rects contained in this RectanglePacker. */ + public void visit(final RectVisitor visitor) { + levels.visit(visitor); + } + + /** Returns the vertical fragmentation ratio of this + RectanglePacker. This is defined as the ratio of the sum of the + heights of all completely empty Levels divided by the overall + used height of the LevelSet. A high vertical fragmentation ratio + indicates that it may be profitable to perform a compaction. */ + public float verticalFragmentationRatio() { + return levels.verticalFragmentationRatio(); + } + + /** Forces a compaction cycle, which typically results in allocating + a new backing store and copying all entries to it. */ + public void compact() { + compactImpl(null); + } + + // The "cause" rect may be null + private void compactImpl(final Rect cause) { + // Have to either expand, compact or both. Need to figure out what + // direction to go. Prefer to expand vertically. Expand + // horizontally only if rectangle being added is too wide. FIXME: + // may want to consider rebalancing the width and height to be + // more equal if it turns out we keep expanding in the vertical + // direction. + boolean done = false; + int newWidth = levels.w(); + int newHeight = levels.h(); + LevelSet nextLevelSet = null; + int attemptNumber = 0; + boolean needAdditionFailureNotification = false; + + while (!done) { + if (cause != null) { + if (cause.w() > newWidth) { + newWidth = cause.w(); + } else { + newHeight = (int) (newHeight * (1.0f + EXPANSION_FACTOR)); + } + } + + // Clamp to maximum values + needAdditionFailureNotification = false; + if (maxWidth > 0 && newWidth > maxWidth) { + newWidth = maxWidth; + needAdditionFailureNotification = true; + } + if (maxHeight > 0 && newHeight > maxHeight) { + newHeight = maxHeight; + needAdditionFailureNotification = true; + } + + nextLevelSet = new LevelSet(newWidth, newHeight); + + // Make copies of all existing rectangles + final List newRects = new ArrayList(); + for (final Iterator i1 = levels.iterator(); i1.hasNext(); ) { + final Level level = i1.next(); + for (final Iterator i2 = level.iterator(); i2.hasNext(); ) { + final Rect cur = i2.next(); + final Rect newRect = new Rect(0, 0, cur.w(), cur.h(), null); + cur.setNextLocation(newRect); + // Hook up the reverse mapping too for easier replacement + newRect.setNextLocation(cur); + newRects.add(newRect); + } + } + // Sort them by decreasing height (note: this isn't really + // guaranteed to improve the chances of a successful layout) + Collections.sort(newRects, rectHComparator); + // Try putting all of these rectangles into the new level set + done = true; + for (final Iterator iter = newRects.iterator(); iter.hasNext(); ) { + if (!nextLevelSet.add(iter.next())) { + done = false; + break; + } + } + + if (done && cause != null) { + // Try to add the new rectangle as well + if (nextLevelSet.add(cause)) { + // We're OK + } else { + done = false; + } + } + + // Don't send addition failure notifications if we're only doing + // a compaction + if (!done && needAdditionFailureNotification && cause != null) { + manager.additionFailed(cause, attemptNumber); + } + ++attemptNumber; + } + + // See whether the implicit compaction that just occurred has + // yielded excess empty space. + if (nextLevelSet.getUsedHeight() > 0 && + nextLevelSet.getUsedHeight() < nextLevelSet.h() * SHRINK_FACTOR) { + int shrunkHeight = Math.max(initialHeight, + (int) (nextLevelSet.getUsedHeight() * (1.0f + EXPANSION_FACTOR))); + if (maxHeight > 0 && shrunkHeight > maxHeight) { + shrunkHeight = maxHeight; + } + nextLevelSet.setHeight(shrunkHeight); + } + + // If we temporarily added the new rectangle to the new LevelSet, + // take it out since we don't "really" add it here but in add(), above + if (cause != null) { + nextLevelSet.remove(cause); + } + + // OK, now we have a new layout and a mapping from the old to the + // new locations of rectangles on the backing store. Allocate a + // new backing store, move the contents over and deallocate the + // old one. + final Object newBackingStore = manager.allocateBackingStore(nextLevelSet.w(), + nextLevelSet.h()); + manager.beginMovement(backingStore, newBackingStore); + for (final Iterator i1 = levels.iterator(); i1.hasNext(); ) { + final Level level = i1.next(); + for (final Iterator i2 = level.iterator(); i2.hasNext(); ) { + final Rect cur = i2.next(); + manager.move(backingStore, cur, + newBackingStore, cur.getNextLocation()); + } + } + // Replace references to temporary rectangles with original ones + nextLevelSet.updateRectangleReferences(); + manager.endMovement(backingStore, newBackingStore); + // Now delete the old backing store + manager.deleteBackingStore(backingStore); + // Update to new versions of backing store and LevelSet + backingStore = newBackingStore; + levels = nextLevelSet; + } + + /** Clears all Rects contained in this RectanglePacker. */ + public void clear() { + levels.clear(); + } + + /** Disposes the backing store allocated by the + BackingStoreManager. This RectanglePacker may no longer be used + after calling this method. */ + public void dispose() { + if (backingStore != null) + manager.deleteBackingStore(backingStore); + backingStore = null; + levels = null; + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/texture/ImageType.java b/src/main/java/net/opengrabeso/opengl/util/texture/ImageType.java new file mode 100644 index 00000000..3d9f58ee --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/texture/ImageType.java @@ -0,0 +1,1579 @@ +/** + * Copyright 2014 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package net.opengrabeso.opengl.util.texture; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Image type classification. + *

+ * Allows to classify the {@link ImageType} of an {@link InputStream} via {@link #ImageType(InputStream)} + * or to simply define one {@link ImageType} via {@link #ImageType(String)}. + *

+ * @since 2.3.2 + */ +public class ImageType { + /** + * Minimum number of bytes to determine the image data type, i.e. {@value} bytes. + */ + public static final int MAGIC_MAX_SIZE = 25; + + /** + * Constant which can be used as a file suffix to indicate a JPEG stream, value {@value}. + *
    + *
  • {@code http://www.faqs.org/faqs/jpeg-faq/part1/}
  • + *
  • {@code http://www.iso.org/iso/iso_catalogue/catalogue_tc/catalogue_detail.htm?csnumber=54989}
  • + *
+ */ + public static final String T_JPG = "jpg"; + + /** + * Constant which can be used as a file suffix to indicate a PNG stream, value {@value}. + *
    + *
  • {@code http://www.libpng.org/pub/png/spec/1.1/PNG-Rationale.html#R.PNG-file-signature}
  • + *
+ */ + public static final String T_PNG = "png"; + + /** + * Constant which can be used as a file suffix to indicate an Apple Icon Image stream, value {@value}. + *

+ * {@code 'i' 'c' 'n' 's' ascii code} + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_ICNS = "icns"; + + /** + * Constant which can be used as a file suffix to indicate a Microsoft Windows Icon stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code https://msdn.microsoft.com/en-us/library/ms997538.aspx}
  • + *
+ */ + public static final String T_ICO = "ico"; + + /** + * Constant which can be used as a file suffix to indicate a Microsoft Windows Cursor stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_CUR = "cur"; + + /** + * Constant which can be used as a file suffix to indicate a GIF stream, value {@value}. + *

+ * {@code GIF87A or GIF89A ascii code} + *

+ *
    + *
  • {@code http://www.w3.org/Graphics/GIF/spec-gif87a.txt http://www.w3.org/Graphics/GIF/spec-gif89a.txt}
  • + *
+ */ + public static final String T_GIF = "gif"; + + /** + * Constant which can be used as a file suffix to indicate a GIF stream, value {@value}. + *

+ * {@code BM ascii code} + *

+ *

+ * FIXME: Collision or supertype of {@link #T_DIB}? + *

+ *
    + *
  • {@code http://www.fileformat.info/format/bmp/spec/e27073c25463436f8a64fa789c886d9c/view.htm}
  • + *
+ */ + public static final String T_BMP = "bmp"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * FIXME: Collision or subtype of {@link #T_BMP}? + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_DIB = "dib"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_DCX = "dcx"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_PCX = "pcx"; + + /** + * Constant which can be used as a file suffix to indicate a PAM stream, NetPbm magic 6 - binary RGB. + *
    + *
  • {@code http://netpbm.sourceforge.net/doc/ppm.html}
  • + *
+ */ + public static final String T_PPM = "ppm"; + + /** + * Constant which can be used as a file suffix to indicate a Adobe PhotoShop stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_PSD = "psd"; + + /** + * Constant which can be used as a file suffix to indicate a TIFF stream, value {@value}. + *

+ * Intentionally detects only the little endian tiff images ("II" in the spec). + *

+ *

+ * FIXME: Collision or supertype of {@link #T_LDF}? + *

+ *
    + *
  • {@code http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf}
  • + *
+ */ + public static final String T_TIFF = "tiff"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * FIXME: Collision or subtype of {@link #T_TIFF}? + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_LDF = "ldf"; + + /** + * Constant which can be used as a file suffix to indicate an SGI RGB stream, value {@value}. + *

+ * "474 saved as a short" 474 = 0x01DA + *

+ *
    + *
  • {@code http://paulbourke.net/dataformats/sgirgb/sgiversion.html}
  • + *
+ */ + public static final String T_SGI_RGB = "rgb"; + + /** + * Constant which can be used as a file suffix to indicate a DirectDraw Surface stream, value {@value}. + *

+ * 'D' 'D' 'S' ' ' ascii code + *

+ *
    + *
  • {@code https://msdn.microsoft.com/en-us/library/windows/desktop/bb943991%28v=vs.85%29.aspx#File_Layout1}
  • + *
+ */ + public static final String T_DDS = "dds"; + + /** + * Constant which can be used as a file suffix to indicate a Portable Arbitrary Map stream, NetPbm magic 7 - binary RGB and RGBA. + *
    + *
  • {@code http://netpbm.sourceforge.net/doc/pam.html}
  • + *
+ */ + public static final String T_PAM = "pam"; + + /** + * Constant which can be used as a file suffix to indicate a PGM stream, NetPbm magic 5 - binary grayscale. + *
    + *
  • {@code http://netpbm.sourceforge.net/doc/pgm.html}
  • + *
+ */ + public static final String T_PGM = "pgm"; + + /** + * Constant which can be used as a file suffix to indicate a PGM stream, NetPbm magic 4 - binary monochrome. + *
    + *
  • {@code http://netpbm.sourceforge.net/doc/pbm.html}
  • + *
+ */ + public static final String T_PBM = "pbm"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_3D2 = "3d2"; + + /** + * Constant which can be used as a file suffix to indicate an Apple QuickDraw 3D 3DMF stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_3DMF = "3dmf"; + + /** + * Constant which can be used as a file suffix to indicate a Texas Instruments TI-92 Bitmap stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_92I = "92i"; + + /** + * Constant which can be used as a file suffix to indicate an Amiga metafile stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_AMFF = "amff"; + + /** + * Constant which can be used as a file suffix to indicate an America Online Art stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_ART = "art"; + + /** + * Constant which can be used as a file suffix to indicate a United States Department of Defence Continuous Acquisition and Life-cycle Support Raster stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code http://www.fileformat.info/format/cals/egff.htm}
  • + *
+ */ + public static final String T_CALS = "cals"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_CAM = "cam"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_CBD = "cbd"; + + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_CE2 = "ce2"; + + /** + * Constant which can be used as a file suffix to indicate a Kodak Cineon System stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code http://www.cineon.com/ff_draft.php}
  • + *
+ */ + public static final String T_CIN = "cin"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_COB = "cob"; + + /** + * Constant which can be used as a file suffix to indicate a Corel Photo Paint stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_CPT = "cpt"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_CVG = "cvg"; + + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_DEM = "dem"; + + /** + * Constant which can be used as a file suffix to indicate a Digital Picture Exchange stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_DPX = "dpx"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_DRW = "drw"; + + /** + * Constant which can be used as a file suffix to indicate a Autocad drawing stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_DWG = "dwg"; + + /** + * Constant which can be used as a file suffix to indicate a Hexagon Geospatial Enhanced Compression Wavelet stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_ECW = "ecw"; + + /** + * Constant which can be used as a file suffix to indicate a Microsoft Windows Enhanced metafile stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_EMF = "emf"; + + /** + * Constant which can be used as a file suffix to indicate a FlashPix stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_FPX = "fpx"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_FTS = "fts"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_GRO = "gro"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_HDR = "hdr"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_HRU = "hru"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_IMG = "img"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_INFINI_D = "infini-d"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_IWC = "iwc"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_J6I = "j6i"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_JIF = "jif"; + + /** + * Constant which can be used as a file suffix to indicate a JPEG-2000 stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_JP2 = "jp2"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_KDC = "kdc"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_L64 = "l64"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * FIXME: Collision or supertype of {@link #T_RAD}? + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_LBM = "lbm"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * FIXME: Collision or subtype of {@link #T_LBM}? + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_RAD = "rad"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_LWF = "lwf"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_MBM = "mbm"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_MGL = "mgl"; + + /** + * Constant which can be used as a file suffix to indicate an Imagemagick stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_MIF = "mif"; + + /** + * Constant which can be used as a file suffix to indicate a Multiple-image Network Graphics stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_MNG = "mng"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_MPW = "mpw"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_MSP = "msp"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_N64 = "n64"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_NCR = "ncr"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_NFF = "nff"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_NGG = "ngg"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_NLM = "nlm"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_NOL = "nol"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_PAL = "pal"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_PAX = "pax"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_PCD = "pcd"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_PCL = "pcl"; + + /** + * Constant which can be used as a file suffix to indicate a Softimage pic stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code http://paulbourke.net/dataformats/softimagepic/}
  • + *
+ */ + public static final String T_PIC = "pic"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_PIX = "pix"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_POL = "pol"; + + /** + * Constant which can be used as a file suffix to indicate a PaintShop Pro stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_PSP = "psp"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_QFX = "qfx"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_QTM = "qtm"; + + /** + * Constant which can be used as a file suffix to indicate a Sun Raster stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_RAS = "ras"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_RIX = "rix"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_SID = "sid"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_SLD = "sld"; + + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_SOD = "sod"; + + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_WIC = "wic"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_WLM = "wlm"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_WMF = "wmf"; + + /** + * Constant which can be used as a file suffix to indicate a Wordperfect Graphics vectors stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_WPG = "wpg"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_WRL = "wrl"; + + /** + * Constant which can be used as a file suffix to indicate a {@code TBD} stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_XBM = "xbm"; + + /** + * Constant which can be used as a file suffix to indicate a X PixMap stream, value {@value}. + *

+ * TODO + *

+ *
    + *
  • {@code TODO}
  • + *
+ */ + public static final String T_XPM = "xpm"; + + /** + * Constant which can be used as a file suffix to indicate a Targa stream, value {@value}. + *
    + *
  • {@code }
  • + *
+ */ + public static final String T_TGA = "tga"; + + /** + * The determined unique type, e.g. {@link #T_PNG}, {@link #T_JPG}, etc. + *

+ * Maybe {@code null} if undetermined, i.e. {@link #isDefined()} returns {@code false}. + *

+ */ + public final String type; + + /** + * The optionally read header of size {@link #MAGIC_MAX_SIZE} bytes as used to determine the {@link #type}, + * i.e. {@link #ImageType(InputStream)}. + *

+ * May be {@code null}, if {@link #type} has been determined otherwise, i.e {@link #ImageType(String)}. + *

+ *

+ * The header is not being used for {@link #hashCode()} and {@link #equals(Object)}! + *

+ */ + public final byte[] header; + + private final int hash; + + /** + * Creates instance based on given stream. + * @param stream stream to parse, {@link InputStream#available()} must be ≥ {@link #MAGIC_MAX_SIZE} + * @throws IOException if an I/O exception occurred + */ + public ImageType(final InputStream stream) throws IOException { + final byte[] _header = new byte[MAGIC_MAX_SIZE]; + type = Util.getFileSuffix(stream, _header); + this.header = _header; + this.hash = null != this.type ? this.type.hashCode() : 0; + } + /** + * Creates instance based on the given type. + * @param type must be one of {@link #T_PNG}, {@link #T_JPG}, etc. + */ + public ImageType(final String type) { + this.header = null; + this.type = type; + this.hash = this.type.hashCode(); + } + /** Returns {@code true} if {@link #type} is determined, i.e. not {@code null}, otherwise {@code false}. */ + public final boolean isDefined() { return null != type; } + + @Override + public final int hashCode() { + return hash; + } + @Override + public boolean equals(final Object o) { + if( o == this ) { + return true; + } else if( o instanceof ImageType ) { + final ImageType t = (ImageType)o; + return this.type.equals(t.type); + } else { + return false; + } + } + @Override + public String toString() { return "ImageType["+type+"]"; } + + /** + * Static utility functions for {@link ImageType} + * to determine the {@link ImageType#type}. + * @since 2.3.2 + */ + public static class Util { + /** + * Determines the file suffix (i.e the image format) of the given InputStream. The given + * InputStream must return true from markSupported() and support a minimum of {@link #MAGIC_MAX_SIZE} bytes + * of read-ahead. + * + * @param stream stream to parse, {@link InputStream#available()} must be ≥ {@link #MAGIC_MAX_SIZE} + * @return the file suffix if any, otherwise null + * @throws IOException if an I/O exception occurred + */ + public static String getFileSuffix(final InputStream stream) throws IOException { + return getFileSuffix(stream, new byte[MAGIC_MAX_SIZE]); + } + /** + * Determines the file suffix (i.e the image format) of the given InputStream. The given + * InputStream must return true from markSupported() and support a minimum of {@link #MAGIC_MAX_SIZE} bytes + * of read-ahead. + * + * @param stream stream to parse, {@link InputStream#available()} must be ≥ {@link #MAGIC_MAX_SIZE} + * @param b byte array sink, size must be ≥ {@link #MAGIC_MAX_SIZE} + * @return the file suffix if any, otherwise null + * @throws IOException if an I/O exception occurred + */ + public static String getFileSuffix(InputStream stream, final byte[] b) throws IOException { + if (stream == null) { + throw new IOException("Stream was null"); + } + if (!(stream instanceof BufferedInputStream)) { + stream = new BufferedInputStream(stream); + } + if (!stream.markSupported()) { + throw new IOException("Mark not supported"); + } + if (stream.available() < MAGIC_MAX_SIZE) { + throw new IOException("Requires "+MAGIC_MAX_SIZE+" bytes, has "+stream.available()+" bytes"); + } + try { + stream.mark(MAGIC_MAX_SIZE); + final int bytesRead = stream.read(b); + if( MAGIC_MAX_SIZE > bytesRead ) { + throw new IOException("Could not read "+MAGIC_MAX_SIZE+" bytes, read "+bytesRead+" bytes"); + } + return getFileSuffix(b); + } finally { + stream.reset(); + } + + } + + /** + * Determines the file suffix (i.e the image format) of the given bytes from the header + * of a file. + * + * @param b byte array to parse, size must be ≥ {@link #MAGIC_MAX_SIZE} + * @return the file suffix if any, otherwise null + * @throws IOException if an I/O exception occurred + */ + public static String getFileSuffix(final byte[] b) { + if( b.length < MAGIC_MAX_SIZE ) { + throw new IllegalArgumentException("byte array must be >= "+MAGIC_MAX_SIZE+", has "+b.length); + } + final byte b0 = b[0]; + final byte b1 = b[1]; + final byte b2 = b[2]; + final byte b3 = b[3]; + final byte b4 = b[4]; + final byte b5 = b[5]; + + // T_TGA: NO Signature! + + if (b0 == (byte)0x00) { + if (b1 == (byte)0x00 && b2 == (byte)0x00 && b3 == (byte)0x0C && + b4 == (byte)0x6A && b5 == (byte)0x50 && + b[6] == (byte)0x20 && b[7] == (byte)0x20 && b[8] == (byte)0x0D && b[9] == (byte)0x0A && b[10] == (byte)0x87 && + b[11] == (byte)0x0A) { + return T_JP2; + } + else if (b1 == (byte)0x01) { + return T_ICO; + } + else if (b1 == (byte)0x02) { + return T_CUR; + } + } + else if (b0 == (byte)0x01) { + if (b1 == (byte)0xDA /* && b2 == (byte)0x01 && b3 == (byte)0x01 && b4 == (byte)0x00 && b5 == (byte)0x03 */) { + return T_SGI_RGB; + } + else if (b1 == (byte)0xFF && b2 == (byte)0x02 && b3 == (byte)0x04 && + b4 == (byte)0x03 && b5 == (byte)0x02) { + return T_DRW; + } + else if (b1 == (byte)0x00 && b2 == (byte)0x00 && b3 == (byte)0x00 && + b4 == (byte)0x58 && b5 == (byte)0x00 && + b[6] == (byte)0x00 && b[7] == (byte)0x00) { + return T_EMF; + } + } + else if (b0 == (byte)0x07 && b1 == (byte)0x20 && b2 == (byte)0x4D && b3 == (byte)0x4D) { + return T_CAM; + } + else if (b0 == (byte)0x0A && b1 == (byte)0x05 && b2 == (byte)0x01 && b3 == (byte)0x08) { + return T_PCX; + } + else if (b0 == (byte)0x1B && b1 == (byte)0x45 && b2 == (byte)0x1B && b3 == (byte)0x26 && + b4 == (byte)0x6C && b5 == (byte)0x30 && + b[6] == (byte)0x4F && b[7] == (byte)0x1B && b[8] == (byte)0x26 && b[9] == (byte)0x6C && b[10] == (byte)0x30 && + b[11] == (byte)0x45 && b[12] == (byte)0x1B && b[13] == (byte)0x26) { + return T_PCL; + } + else if (b0 == (byte)0x20 && b1 == (byte)0x77 && b2 == (byte)0x00 && b3 == (byte)0x02) { + return T_CBD; + } + else if (b0 == (byte)0x23) { + if (b1 == (byte)0x20 && b2 == (byte)0x24 && b3 == (byte)0x49 && + b4 == (byte)0x64 && b5 == (byte)0x3A && + b[6] == (byte)0x20) { + return T_SID; + } + else if (b1 == (byte)0x56 && b2 == (byte)0x52 && b3 == (byte)0x4D && + b4 == (byte)0x4C && b5 == (byte)0x20 && + b[6] == (byte)0x56 && b[7] == (byte)0x32 && b[8] == (byte)0x2E && b[9] == (byte)0x30) { + return T_WRL; + } + else if (b1 == (byte)0x64 && b2 == (byte)0x65 && b3 == (byte)0x66 && + b4 == (byte)0x69 && b5 == (byte)0x6E && + b[6] == (byte)0x65) { + return T_XBM; + } + } + else if (b0 == (byte)0x2A && b1 == (byte)0x2A && b2 == (byte)0x54 && b3 == (byte)0x49 && + b4 == (byte)0x39 && b5 == (byte)0x32 && + b[6] == (byte)0x2A && b[7] == (byte)0x2A && b[8] == (byte)0x01 && b[9] == (byte)0x00 && b[10] == (byte)0x58 && + b[11] == (byte)0x6E && b[12] == (byte)0x56 && b[13] == (byte)0x69) { + return T_92I; + } + else if (b0 == (byte)0x2F && b1 == (byte)0x2A && b2 == (byte)0x20 && b3 == (byte)0x58 && + b4 == (byte)0x50 && b5 == (byte)0x4D && + b[6] == (byte)0x20 && b[7] == (byte)0x2A && b[8] == (byte)0x2F) { + return T_XPM; + } + else if (b0 == (byte)0x33 && b1 == (byte)0x44 && b2 == (byte)0x4D && b3 == (byte)0x46) { + return T_3DMF; + } + else if (b0 == (byte)0x35 && b1 == (byte)0x4B && b2 == (byte)0x50 && b3 == (byte)0x35 && + b4 == (byte)0x31 && b5 == (byte)0x5D && + b[6] == (byte)0x2A && b[7] == (byte)0x67 && b[8] == (byte)0x72 && b[9] == (byte)0x72 && b[10] == (byte)0x80 && + b[11] == (byte)0x83 && b[12] == (byte)0x85 && b[13] == (byte)0x63) { + return T_HRU; + } + else if (b0 == (byte)0x36 && b1 == (byte)0x34 && b2 == (byte)0x4C && b3 == (byte)0x41 && + b4 == (byte)0x4E && b5 == (byte)0x20 && + b[6] == (byte)0x49 && b[7] == (byte)0x44 && b[8] == (byte)0x42 && b[9] == (byte)0x4C && b[10] == (byte)0x4F && + b[11] == (byte)0x43 && b[12] == (byte)0x4B) { + return T_L64; + } + else if (b0 == (byte)0x37 && b1 == (byte)0x00 && b2 == (byte)0x00 && b3 == (byte)0x10 && + b4 == (byte)0x42 && b5 == (byte)0x00 && + b[6] == (byte)0x00 && b[7] == (byte)0x10 && b[8] == (byte)0x00 && b[9] == (byte)0x00 && b[10] == (byte)0x00 && + b[11] == (byte)0x00 && b[12] == (byte)0x39 && b[13] == (byte)0x64) { + return T_MBM; + } + else if (b0 == (byte)0x38 && b1 == (byte)0x42 && b2 == (byte)0x50 && b3 == (byte)0x53 && + b4 == (byte)0x00 && b5 == (byte)0x01 && + b[6] == (byte)0x00 && b[7] == (byte)0x00 && b[8] == (byte)0x00 && b[9] == (byte)0x00) { + return T_PSD; + } + else if (b0 == (byte)0x3A && b1 == (byte)0xDE && b2 == (byte)0x68 && b3 == (byte)0xB1) { + return T_DCX; + } + else if (b0 == (byte)0x3D && b1 == (byte)0x02) { + return T_3D2; + } + else if (b0 == (byte)0x41) { + if (b1 == (byte)0x43 && b2 == (byte)0x31 && b3 == (byte)0x30) { + return T_DWG; + } + else if (b1 == (byte)0x48) { + return T_PAL; + } + else if (b1 == (byte)0x4D && b2 == (byte)0x46 && b3 == (byte)0x46) { + return T_AMFF; + } + else if (b1 == (byte)0x75 && b2 == (byte)0x74 && b3 == (byte)0x6F && + b4 == (byte)0x43 && b5 == (byte)0x41 && + b[6] == (byte)0x44 && b[7] == (byte)0x20 && b[8] == (byte)0x53 && b[9] == (byte)0x6C && b[10] == (byte)0x69 && + b[11] == (byte)0x64 && b[12] == (byte)0x65) { + return T_SLD; + } + } + else if (b0 == (byte)0x42 && b1 == (byte)0x4D) { + if (b2 == (byte)0x36) { + // FIXME: Collision or subtype of T_BMP? + return T_DIB; + } else { + return T_BMP; + } + } + else if (b0 == (byte)0x43) { + if (b1 == (byte)0x36 && b2 == (byte)0x34) { + return T_N64; + } + else if (b1 == (byte)0x41 && b2 == (byte)0x4C && b3 == (byte)0x41 && + b4 == (byte)0x4D && b5 == (byte)0x55 && + b[6] == (byte)0x53 && b[7] == (byte)0x43 && b[8] == (byte)0x56 && b[9] == (byte)0x47) { + return T_CVG; + } + else if (b1 == (byte)0x50 && b2 == (byte)0x54 && b3 == (byte)0x46 && + b4 == (byte)0x49 && b5 == (byte)0x4C && + b[6] == (byte)0x45) { + return T_CPT; + } + else if (b1 == (byte)0x61 && b2 == (byte)0x6C && b3 == (byte)0x69 && + b4 == (byte)0x67 && b5 == (byte)0x61 && + b[6] == (byte)0x72 && b[7] == (byte)0x69) { + return T_COB; + } + } + else if (b0 == (byte)0x44) { + if (b1 == (byte)0x44 && b2 == (byte)0x53 && b3 == (byte)0x20) { + return T_DDS; + } + else if (b1 == (byte)0x61 && b2 == (byte)0x6E && b3 == (byte)0x4D) { + return T_MSP; + } + } + else if (b0 == (byte)0x45) { + if (b1 == (byte)0x59 && b2 == (byte)0x45 && b3 == (byte)0x53) { + return T_CE2; + } + else if (b1 == (byte)0x78 && b2 == (byte)0x69 && b3 == (byte)0x66) { /* EXIF */ + /** + * (b0 == (byte)0x45 && b1 == (byte)0x78 && b2 == (byte)0x69 && b3 == (byte)0x66) || // EXIF + * (b0 == (byte)0x4A && b1 == (byte)0x46 && b2 == (byte)0x49 && b3 == (byte)0x46) || // JFIF + * (b0 == (byte)0xff && b1 == (byte)0xd8 ) // && b2 == (byte)0xff + */ + return T_JPG; + } + } + else if (b0 == (byte)0x46 && b1 == (byte)0x4F && b2 == (byte)0x52 && b3 == (byte)0x4D) { + if (b4 == (byte)0x41 && b5 == (byte)0x54 && b[6] == (byte)0x3D) { + // FIXME: Collision or subtype of T_LBM? + return T_RAD; + } else { + return T_LBM; + } + } + else if (b0 == (byte)0x47 && b1 == (byte)0x49 && b2 == (byte)0x46 && b3 == (byte)0x38 && + (b4 == (byte)0x37 || b4 == (byte)0x39) && b5 == (byte)0x61) { + return T_GIF; + } + else if (b0 == (byte)0x48 && b1 == (byte)0x50 && b2 == (byte)0x48 && b3 == (byte)0x50 && + b4 == (byte)0x34 && b5 == (byte)0x38 && + b[6] == (byte)0x2D && b[7] == (byte)0x45 && b[8] == (byte)0x1E && b[9] == (byte)0x2B) { + return T_GRO; + } + else if (b0 == (byte)0x49) { + if (b1 == (byte)0x49 && b2 == (byte)0x2A && b3 == (byte)0x00) { + if (b4 == (byte)0x08 && b5 == (byte)0x00 && + b[6] == (byte)0x00 && b[7] == (byte)0x00 && b[8] == (byte)0x0E && b[9] == (byte)0x00 && b[10] == (byte)0x00 && + b[11] == (byte)0x01 && b[12] == (byte)0x04 && b[13] == (byte)0x00) { + // FIXME: Collision or subtype of T_TIFF? + return T_LDF; + } else { + return T_TIFF; + } + } + else if (b1 == (byte)0x57 && b2 == (byte)0x43 && b3 == (byte)0x01) { + return T_IWC; + } + } + else if (b0 == (byte)0x4A) { + if (b1 == (byte)0x46 && b2 == (byte)0x49 && b3 == (byte)0x46) { /* JFIF */ + /** + * (b0 == (byte)0x45 && b1 == (byte)0x78 && b2 == (byte)0x69 && b3 == (byte)0x66) || // EXIF + * (b0 == (byte)0x4A && b1 == (byte)0x46 && b2 == (byte)0x49 && b3 == (byte)0x46) || // JFIF + * (b0 == (byte)0xff && b1 == (byte)0xd8 ) // && b2 == (byte)0xff + */ + return T_JPG; + } + else if (b1 == (byte)0x47 && (b2 == (byte)0x03 || b2 == (byte)0x04) && b3 == (byte)0x0E && + b4 == (byte)0x00 && b5 == (byte)0x00 && + b[6] == (byte)0x00) { + return T_ART; + } + else if (b1 == (byte)0x49 && b2 == (byte)0x46 && b3 == (byte)0x39 && + b4 == (byte)0x39 && b5 == (byte)0x61) { + return T_JIF; + } + } + else if (b0 == (byte)0x4D) { + if (b1 == (byte)0x47 && b2 == (byte)0x4C) { + return T_MGL; + } + else if (b1 == (byte)0x4D && b2 == (byte)0x00 && b3 == (byte)0x2A) { + return T_KDC; + } + else if (b1 == (byte)0x50 && b2 == (byte)0x46) { + return T_MPW; + } + } + else if (b0 == (byte)0x4E) { + if (b1 == (byte)0x47 && b2 == (byte)0x47 && b3 == (byte)0x00 && + b4 == (byte)0x01 && b5 == (byte)0x00) { + return T_NGG; + } + else if (b1 == (byte)0x4C && b2 == (byte)0x4D && b3 == (byte)0x20 && + b4 == (byte)0x01 && b5 == (byte)0x02 && + b[6] == (byte)0x00) { + return T_NLM; + } + else if (b1 == (byte)0x4F && b2 == (byte)0x4C && b3 == (byte)0x00 && + b4 == (byte)0x01 && b5 == (byte)0x00 && + b[6] == (byte)0x06 && b[7] == (byte)0x01 && b[8] == (byte)0x03 && b[9] == (byte)0x00) { + return T_NOL; + } + } + else if (b0 == (byte)0x50) { + if (b1 == (byte)0x31 /* plain */|| b1 == (byte)0x34) { + return T_PBM; + } + else if (b1 == (byte)0x32 /* plain */|| b1 == (byte)0x35) { + return T_PGM; + } + else if (b1 == (byte)0x33 /* plain */|| b1 == (byte)0x36) { + return T_PPM; + } + else if (b1 == (byte)0x37) { + return T_PAM; + } + else if (b1 == (byte)0x41 && b2 == (byte)0x58) { + return T_PAX; + } + else if (b1 == (byte)0x49 && b2 == (byte)0x58 && b3 == (byte)0x20) { + return T_PIX; + } + else if (b1 == (byte)0x4F && b2 == (byte)0x4C && b3 == (byte)0x20 && + b4 == (byte)0x46 && b5 == (byte)0x6F && + b[6] == (byte)0x72 && b[7] == (byte)0x6D && b[8] == (byte)0x61 && b[9] == (byte)0x74) { + return T_POL; + } + else if (b1 == (byte)0x61 && b2 == (byte)0x69 && b3 == (byte)0x6E && + b4 == (byte)0x74 && b5 == (byte)0x20 && + b[6] == (byte)0x53 && b[7] == (byte)0x68 && b[8] == (byte)0x6F && b[9] == (byte)0x70 && b[10] == (byte)0x20 && + b[11] == (byte)0x50 && b[12] == (byte)0x72 && b[13] == (byte)0x6F && b[14] == (byte)0x20 && b[15] == (byte)0x49 && + b[16] == (byte)0x6D && b[17] == (byte)0x61 && b[18] == (byte)0x67 && b[19] == (byte)0x65 && b[20] == (byte)0x20 && + b[21] == (byte)0x46 && b[22] == (byte)0x69 && b[23] == (byte)0x6C && b[24] == (byte)0x65) { + return T_PSP; + } + } + else if (b0 == (byte)0x51 && b1 == (byte)0x4C && b2 == (byte)0x49 && b3 == (byte)0x49 && + b4 == (byte)0x46 && b5 == (byte)0x41 && + b[6] == (byte)0x58) { + return T_QFX; + } + else if (b0 == (byte)0x52 && b1 == (byte)0x49 && b2 == (byte)0x58 && b3 == (byte)0x33) { + return T_RIX; + } + else if (b0 == (byte)0x53) { + if (b1 == (byte)0x44 && b2 == (byte)0x50 && b3 == (byte)0x58) { + return T_DPX; + } + else if (b1 == (byte)0x49 && b2 == (byte)0x4D && b3 == (byte)0x50 && + b4 == (byte)0x4C && b5 == (byte)0x45 && + b[6] == (byte)0x20 && b[7] == (byte)0x20 && b[8] == (byte)0x3D) { + return T_FTS; + } + else if (b1 == (byte)0x74 && b2 == (byte)0x6F && b3 == (byte)0x72 && + b4 == (byte)0x6D && b5 == (byte)0x33 && + b[6] == (byte)0x44) { + return T_SOD; + } + else if (b1 == (byte)0x80 && b2 == (byte)0xf6 && b3 == (byte)0x34) { + return T_PIC; + } + } + else if (b0 == (byte)0x56 && b1 == (byte)0x69 && b2 == (byte)0x73 && b3 == (byte)0x74 && + b4 == (byte)0x61 && b5 == (byte)0x20 && + b[6] == (byte)0x44 && b[7] == (byte)0x45 && b[8] == (byte)0x4D && b[9] == (byte)0x20 && b[10] == (byte)0x46 && + b[11] == (byte)0x69 && b[12] == (byte)0x6C && b[13] == (byte)0x65) { + return T_DEM; + } + else if (b0 == (byte)0x57 && b1 == (byte)0x56 && b2 == (byte)0x02 && b3 == (byte)0x00 && + b4 == (byte)0x47 && b5 == (byte)0x45 && + b[6] == (byte)0x00 && b[7] == (byte)0x0E) { + return T_LWF; + } + else if (b0 == (byte)0x59 && b1 == (byte)0xA6 && b2 == (byte)0x6A && b3 == (byte)0x95) { + return T_RAS; + } + else if (b0 == (byte)0x63 && b1 == (byte)0x52 && b2 == (byte)0x01 && b3 == (byte)0x01 && + b4 == (byte)0x38 && b5 == (byte)0x09 && + b[6] == (byte)0x3D && b[7] == (byte)0x00) { + return T_PCD; + } + else if (b0 == (byte)0x65) { + if (b1 == (byte)0x02 && b2 == (byte)0x01 && b3 == (byte)0x02) { + return T_ECW; + } + else if (b1 == (byte)0x6C && b2 == (byte)0x6D && b3 == (byte)0x6F) { + return T_INFINI_D; + } + } + else if (b0 == (byte)0x69 && b1 == (byte)0x63 && b2 == (byte)0x6E && b3 == (byte)0x73) { + return T_ICNS; + } + else if (b0 == (byte)0x6D && b1 == (byte)0x6F && b2 == (byte)0x6F && b3 == (byte)0x76) { + return T_QTM; + } + else if (b0 == (byte)0x6E) { + if (b1 == (byte)0x63 && b2 == (byte)0x6F && b3 == (byte)0x6C && + b4 == (byte)0x73) { + return T_HDR; + } + else if (b1 == (byte)0x66 && b2 == (byte)0x66) { + return T_NFF; + } + else if (b1 == (byte)0x6E && b2 == (byte)0x0A && b3 == (byte)0x00 && + b4 == (byte)0x5E && b5 == (byte)0x00) { + return T_NCR; + } + } + else if (b0 == (byte)0x73 && b1 == (byte)0x72 && b2 == (byte)0x63 && b3 == (byte)0x64 && + b4 == (byte)0x6F && b5 == (byte)0x63 && + b[6] == (byte)0x69 && b[7] == (byte)0x64 && b[8] == (byte)0x3A) { + return T_CALS; + } + else if (b0 == (byte)0x7B && b1 == (byte)0x0A && b2 == (byte)0x20 && b3 == (byte)0x20 && + b4 == (byte)0x43 && b5 == (byte)0x72 && + b[6] == (byte)0x65 && b[7] == (byte)0x61 && b[8] == (byte)0x74 && b[9] == (byte)0x65 && b[10] == (byte)0x64) { + return T_MIF; + } + else if (b0 == (byte)0x7E && b1 == (byte)0x42 && b2 == (byte)0x4B && b3 == (byte)0x00) { + return T_PSP; + } + else if (b0 == (byte)0x80) { + if (b1 == (byte)0x2A && b2 == (byte)0x5F && b3 == (byte)0xD7 && + b4 == (byte)0x00 && b5 == (byte)0x00 && + b[6] == (byte)0x08 && b[7] == (byte)0x00 && b[8] == (byte)0x00 && b[9] == (byte)0x00 && b[10] == (byte)0x04 && + b[11] == (byte)0x00 && b[12] == (byte)0x00 && b[13] == (byte)0x00) { + return T_CIN; + } + else if (b1 == (byte)0x3E && b2 == (byte)0x44 && b3 == (byte)0x53 && + b4 == (byte)0x43 && b5 == (byte)0x49 && + b[6] == (byte)0x4D) { + return T_J6I; + } + } + else if (b0 == (byte)0x89 && b1 == (byte)0x50 && b2 == (byte)0x4E && b3 == (byte)0x47 && /* 'P' 'N' 'G', ascii code */ + b4 == (byte)0x0D && b5 == (byte)0x0A && b[6] == (byte)0x1A && b[7] == (byte)0x0A) { + // -119, 80, 78, 71, 13, 10, 26, 10 + return T_PNG; + } + else if (b0 == (byte)0x8A && b1 == (byte)0x4D && b2 == (byte)0x4E && b3 == (byte)0x47 && + b4 == (byte)0x0D && b5 == (byte)0x0A && + b[6] == (byte)0x1A && b[7] == (byte)0x0A) { + return T_MNG; + } + else if (b0 == (byte)0xD0 && b1 == (byte)0xCF && b2 == (byte)0x11 && b3 == (byte)0xE0 && + b4 == (byte)0xA1 && b5 == (byte)0xB1 && + b[6] == (byte)0x1A && b[7] == (byte)0xE1 && b[8] == (byte)0x00) { + return T_FPX; + } + else if (b0 == (byte)0xD3 && b1 == (byte)0x23 && b2 == (byte)0x00 && b3 == (byte)0x00 && + b4 == (byte)0x03 && b5 == (byte)0x00 && + b[6] == (byte)0x00 && b[7] == (byte)0x00) { + return T_WLM; + } + else if (b0 == (byte)0xD7 && b1 == (byte)0xCD && b2 == (byte)0xC6 && b3 == (byte)0x9A) { + return T_WMF; + } + else if (b0 == (byte)0xEB && b1 == (byte)0x3C && b2 == (byte)0x90 && b3 == (byte)0x2A) { + return T_IMG; + } + else if (b0 == (byte)0xFA && b1 == (byte)0xDE && b2 == (byte)0xBA && b3 == (byte)0xBE && + b4 == (byte)0x01 && b5 == (byte)0x01) { + return T_WIC; + } + else if (b0 == (byte)0xFF) { + if (b1 == (byte)0xD8 /* && b2 == (byte)0xff */) { + /** + * (b0 == (byte)0x45 && b1 == (byte)0x78 && b2 == (byte)0x69 && b3 == (byte)0x66) || // EXIF + * (b0 == (byte)0x4A && b1 == (byte)0x46 && b2 == (byte)0x49 && b3 == (byte)0x46) || // JFIF + * (b0 == (byte)0xff && b1 == (byte)0xd8 ) // && b2 == (byte)0xff + */ + return T_JPG; + } + else if (b1 == (byte)0x57 && b2 == (byte)0x50 && b3 == (byte)0x43 && b4 == (byte)0x10) { + return T_WPG; + } + } + return null; + } + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/texture/Texture.java b/src/main/java/net/opengrabeso/opengl/util/texture/Texture.java new file mode 100644 index 00000000..b75451fe --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/texture/Texture.java @@ -0,0 +1,843 @@ +/* + * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. + * Copyright (c) 2010 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + */ + +package net.opengrabeso.opengl.util.texture; + +import com.github.opengrabeso.jaagl.GL; + +import java.awt.image.DataBufferByte; +import java.nio.*; + +/** + * Represents an OpenGL texture object. Contains convenience routines + * for enabling/disabling OpenGL texture state, binding this texture, + * and computing texture coordinates for both the entire image as well + * as a sub-image. + * + *
Order of Texture Commands
+ *

+ * Due to many confusions w/ texture usage, following list described the order + * and semantics of texture unit selection, binding and enabling. + *

    + *
  • Optional: Set active textureUnit via gl.glActiveTexture(GL.GL_TEXTURE0 + textureUnit), 0 is default.
  • + *
  • Bind textureId -> active textureUnit's textureTarget via gl.glBindTexture(textureTarget, textureId)
  • + *
  • Compatible Context Only: Enable active textureUnit's textureTarget via glEnable(textureTarget). + *
  • Optional: Fiddle with the texture parameters and/or environment settings.
  • + *
  • GLSL: Use textureUnit in your shader program, enable shader program.
  • + *
  • Issue draw commands
  • + *
+ *

+ * + *

Non-power-of-two restrictions + *
When creating an OpenGL texture object, the Texture class will + * attempt to use non-power-of-two textures. + * + *

Performance Tips + *
For best performance, try to avoid calling {@link #enable} / + * {@link #bind} / {@link #disable} any more than necessary. For + * example, applications using many Texture objects in the same scene + * may want to reduce the number of calls to both {@link #enable} and + * {@link #disable}. To do this it is necessary to call {@link + * #getTarget} to make sure the OpenGL texture target is the same for + * all of the Texture objects in use; non-power-of-two textures using + * the GL_ARB_texture_rectangle extension use a different target than + * power-of-two textures using the GL_TEXTURE_2D target. Note that + * when switching between textures it is necessary to call {@link + * #bind}, but when drawing many triangles all using the same texture, + * for best performance only one call to {@link #bind} should be made. + * User may also utilize multiple texture units, + * see order of texture commands above. + * + *

Alpha premultiplication and blending + *

+ * Disclaimer: Consider performing alpha premultiplication in shader code, if really desired! Otherwise use RGBA.
+ *

+ *

+ * The Texture class does not convert RGBA image data into + * premultiplied data when storing it into an OpenGL texture. + *

+ *

+ * The mathematically correct way to perform blending in OpenGL + * with the SrcOver "source over destination" mode, or any other + * Porter-Duff rule, is to use premultiplied color components, + * which means the R/G/ B color components must have been multiplied by + * the alpha value. If using premultiplied color components + * it is important to use the correct blending function; for + * example, the SrcOver rule is expressed as: +

+    gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA);
+
+ * Also, when using a texture function like GL_MODULATE where + * the current color plays a role, it is important to remember to make + * sure that the color is specified in a premultiplied form, for + * example: +
+    float a = ...;
+    float r = r * a;
+    float g = g * a;
+    float b = b * a;
+    gl.glColor4f(r, g, b, a);
+
+ * + * For reference, here is a list of the Porter-Duff compositing rules + * and the associated OpenGL blend functions (source and destination + * factors) to use in the face of premultiplied alpha: + * +
+ +
Rule Source Dest +
Clear GL_ZERO GL_ZERO +
Src GL_ONE GL_ZERO +
SrcOver GL_ONE GL_ONE_MINUS_SRC_ALPHA +
DstOver GL_ONE_MINUS_DST_ALPHA GL_ONE +
SrcIn GL_DST_ALPHA GL_ZERO +
DstIn GL_ZERO GL_SRC_ALPHA +
SrcOut GL_ONE_MINUS_DST_ALPHA GL_ZERO +
DstOut GL_ZERO GL_ONE_MINUS_SRC_ALPHA +
Dst GL_ZERO GL_ONE +
SrcAtop GL_DST_ALPHA GL_ONE_MINUS_SRC_ALPHA +
DstAtop GL_ONE_MINUS_DST_ALPHA GL_SRC_ALPHA +
AlphaXor GL_ONE_MINUS_DST_ALPHA GL_ONE_MINUS_SRC_ALPHA +
+
+ * @author Chris Campbell, Kenneth Russell, et.al. + */ +public class Texture { + /** The GL target type for this texture. */ + private int target; + /** The image GL target type for this texture, or its sub-components if cubemap. */ + private int imageTarget; + /** The GL texture ID. */ + private int texID; + /** The width of the texture. */ + private int texWidth; + /** The height of the texture. */ + private int texHeight; + /** The width of the image. */ + private int imgWidth; + /** The height of the image. */ + private int imgHeight; + /** The original aspect ratio of the image, before any rescaling + that might have occurred due to using the GLU mipmap routines. */ + private float aspectRatio; + /** Indicates whether the TextureData requires a vertical flip of + the texture coords. */ + private boolean mustFlipVertically; + /** Indicates whether we're using automatic mipmap generation + support (GL_GENERATE_MIPMAP). */ + private boolean usingAutoMipmapGeneration; + + /** The texture coordinates corresponding to the entire image. */ + private TextureCoords coords; + + @Override + public String toString() { + final String targetS = target == imageTarget ? Integer.toHexString(target) : Integer.toHexString(target) + " - image "+Integer.toHexString(imageTarget); + return "Texture[target "+targetS+", name "+texID+", "+ + imgWidth+"/"+texWidth+" x "+imgHeight+"/"+texHeight+", y-flip "+mustFlipVertically+ + "]"; + } + + public Texture(final GL gl, final TextureData data) { + this.texID = 0; + this.target = 0; + this.imageTarget = 0; + updateImage(gl, data); + } + + /** + * Constructor for use when creating e.g. cube maps, where there is + * no initial texture data + * @param target the OpenGL texture target, eg GL.GL_TEXTURE_2D, + * GL2.GL_TEXTURE_RECTANGLE + */ + public Texture(final int target) { + this.texID = 0; + this.target = target; + this.imageTarget = target; + } + + /** + * Constructor to wrap an OpenGL texture ID from an external library and allows + * some of the base methods from the Texture class, such as + * binding and querying of texture coordinates, to be used with + * it. Attempts to update such textures' contents will yield + * undefined results. + * + * @param textureID the OpenGL texture object to wrap + * @param target the OpenGL texture target, eg GL.GL_TEXTURE_2D, + * GL2.GL_TEXTURE_RECTANGLE + * @param texWidth the width of the texture in pixels + * @param texHeight the height of the texture in pixels + * @param imgWidth the width of the image within the texture in + * pixels (if the content is a sub-rectangle in the upper + * left corner); otherwise, pass in texWidth + * @param imgHeight the height of the image within the texture in + * pixels (if the content is a sub-rectangle in the upper + * left corner); otherwise, pass in texHeight + * @param mustFlipVertically indicates whether the texture + * coordinates must be flipped vertically + * in order to properly display the + * texture + */ + public Texture(final int textureID, final int target, + final int texWidth, final int texHeight, + final int imgWidth, final int imgHeight, + final boolean mustFlipVertically) { + this.texID = textureID; + this.target = target; + this.imageTarget = target; + this.mustFlipVertically = mustFlipVertically; + this.texWidth = texWidth; + this.texHeight = texHeight; + this.aspectRatio = (float) imgWidth / (float) imgHeight; + this.imgWidth = imgWidth; + this.imgHeight = imgHeight; + this.updateTexCoords(); + } + + /** + * Enables this texture's target (e.g., GL_TEXTURE_2D) in the + * given GL context's state. This method is a shorthand equivalent + * of the following OpenGL code: + *
+     *   gl.glEnable(texture.getTarget());
+     * 
+ *

+ * Call is ignored if the {@link GL} object's context + * is using a core profile, + *

+ *

+ * See the performance tips above for hints + * on how to maximize performance when using many Texture objects. + *

+ * @param gl the current GL object + * + + * OpenGL-related errors occurred + */ + public void enable(final GL gl) { + if( !gl.isGL3()) { + gl.glEnable(target); + } + } + + /** + * Disables this texture's target (e.g., GL_TEXTURE_2D) in the + * given GL state. This method is a shorthand equivalent + * of the following OpenGL code: + *
+     *   gl.glDisable(texture.getTarget());
+     * 
+ *

+ * Call is ignored if the {@link GL} object's context + * is using a core profile + *

+ *

+ * See the performance tips above for hints + * on how to maximize performance when using many Texture objects. + *

+ * @param gl the current GL object + * + + * OpenGL-related errors occurred + */ + public void disable(final GL gl) { + if( !gl.isGL3()) { + gl.glDisable(target); + } + } + + /** + * Binds this texture to the given GL context. This method is a + * shorthand equivalent of the following OpenGL code: +
+     gl.glBindTexture(texture.getTarget(), texture.getTextureObject());
+     
+ * + * See the performance tips above for hints + * on how to maximize performance when using many Texture objects. + * + * @param gl the current GL context + + * OpenGL-related errors occurred + */ + public void bind(final GL gl) { + validateTexID(gl, true); + gl.glBindTexture(target, texID); + } + + /** + * Destroys the native resources used by this texture object. + * + + */ + public void destroy(final GL gl) { + if(0!=texID) { + gl.glDeleteTextures(new int[] {texID}); + texID = 0; + } + } + + /** + * Returns the OpenGL "target" of this texture. + * @see com.jogamp.opengl.GL#GL_TEXTURE_2D + * @see com.jogamp.opengl.GL2#GL_TEXTURE_RECTANGLE_ARB + */ + public int getTarget() { + return target; + } + + /** + * Returns the image OpenGL "target" of this texture, or its sub-components if cubemap. + * @see com.jogamp.opengl.GL#GL_TEXTURE_2D + * @see com.jogamp.opengl.GL2#GL_TEXTURE_RECTANGLE_ARB + */ + public int getImageTarget() { + return imageTarget; + } + + /** + * Returns the width of the allocated OpenGL texture in pixels. + * Note that the texture width will be greater than or equal to the + * width of the image contained within. + * + * @return the width of the texture + */ + public int getWidth() { + return texWidth; + } + + /** + * Returns the height of the allocated OpenGL texture in pixels. + * Note that the texture height will be greater than or equal to the + * height of the image contained within. + * + * @return the height of the texture + */ + public int getHeight() { + return texHeight; + } + + /** + * Returns the width of the image contained within this texture. + * Note that for non-power-of-two textures in particular this may + * not be equal to the result of {@link #getWidth}. It is + * recommended that applications call {@link #getImageTexCoords} and + * {@link #getSubImageTexCoords} rather than using this API + * directly. + * + * @return the width of the image + */ + public int getImageWidth() { + return imgWidth; + } + + /** + * Returns the height of the image contained within this texture. + * Note that for non-power-of-two textures in particular this may + * not be equal to the result of {@link #getHeight}. It is + * recommended that applications call {@link #getImageTexCoords} and + * {@link #getSubImageTexCoords} rather than using this API + * directly. + * + * @return the height of the image + */ + public int getImageHeight() { + return imgHeight; + } + + /** + * Returns the original aspect ratio of the image, defined as (image + * width) / (image height), before any scaling that might have + * occurred as a result of using the GLU mipmap routines. + */ + public float getAspectRatio() { + return aspectRatio; + } + + /** + * Returns the set of texture coordinates corresponding to the + * entire image. If the TextureData indicated that the texture + * coordinates must be flipped vertically, the returned + * TextureCoords will take that into account. + * + * @return the texture coordinates corresponding to the entire image + */ + public TextureCoords getImageTexCoords() { + return coords; + } + + /** + * Returns the set of texture coordinates corresponding to the + * specified sub-image. The (x1, y1) and (x2, y2) points are + * specified in terms of pixels starting from the lower-left of the + * image. (x1, y1) should specify the lower-left corner of the + * sub-image and (x2, y2) the upper-right corner of the sub-image. + * If the TextureData indicated that the texture coordinates must be + * flipped vertically, the returned TextureCoords will take that + * into account; this should not be handled by the end user in the + * specification of the y1 and y2 coordinates. + * + * @return the texture coordinates corresponding to the specified sub-image + */ + public TextureCoords getSubImageTexCoords(final int x1, final int y1, final int x2, final int y2) { + final float tx1 = (float)x1 / (float)texWidth; + final float ty1 = (float)y1 / (float)texHeight; + final float tx2 = (float)x2 / (float)texWidth; + final float ty2 = (float)y2 / (float)texHeight; + if (mustFlipVertically) { + final float yMax = (float) imgHeight / (float) texHeight; + return new TextureCoords(tx1, yMax - ty1, tx2, yMax - ty2); + } else { + return new TextureCoords(tx1, ty1, tx2, ty2); + } + } + + /** + * Updates the entire content area incl. {@link TextureCoords} + * of this texture using the data in the given image. + * + + */ + public void updateImage(final GL gl, final TextureData data) { + updateImage(gl, data, 0); + } + + /** + * Indicates whether this texture's texture coordinates must be + * flipped vertically in order to properly display the texture. This + * is handled automatically by {@link #getImageTexCoords + * getImageTexCoords} and {@link #getSubImageTexCoords + * getSubImageTexCoords}, but applications may generate or otherwise + * produce texture coordinates which must be corrected. + */ + public boolean getMustFlipVertically() { + return mustFlipVertically; + } + + /** + * Change whether the TextureData requires a vertical flip of + * the texture coords. + *

+ * No-op if no change, otherwise generates new {@link TextureCoords}. + *

+ */ + public void setMustFlipVertically(final boolean v) { + if( v != mustFlipVertically ) { + mustFlipVertically = v; + updateTexCoords(); + } + } + + /** + * Updates the content area incl. {@link TextureCoords} of the specified target of this texture + * using the data in the given image. In general this is intended + * for construction of cube maps. + * + + */ + public void updateImage(final GL gl, final TextureData data, final int targetOverride) { + validateTexID(gl, true); + + imgWidth = data.getWidth(); + imgHeight = data.getHeight(); + aspectRatio = (float) imgWidth / (float) imgHeight; + mustFlipVertically = data.getMustFlipVertically(); + + int texTarget = 0; + int texParamTarget = this.target; + + // See whether we have automatic mipmap generation support + + // Indicates whether both width and height are power of two + final boolean isPOT = true; + + // Note that automatic mipmap generation doesn't work for + // GL_ARB_texture_rectangle + + texWidth = imgWidth; + texHeight = imgHeight; + texTarget = gl.GL_TEXTURE_2D(); + + texParamTarget = texTarget; + imageTarget = texTarget; + updateTexCoords(); + + if (targetOverride != 0) { + // Allow user to override auto detection and skip bind step (for + // cubemap construction) + if (this.target == 0) { + throw gl.newGLException("Override of target failed; no target specified yet"); + } + texTarget = targetOverride; + texParamTarget = this.target; + gl.glBindTexture(texParamTarget, texID); + } else { + gl.glBindTexture(texTarget, texID); + } + + checkCompressedTextureExtensions(gl, data); + final Buffer[] mipmapData = data.getMipmapData(); + if (mipmapData != null) { + int width = texWidth; + int height = texHeight; + for (int i = 0; i < mipmapData.length; i++) { + // Allocate texture image at this level + gl.glTexImage2D(texTarget, i, data.getInternalFormat(), + width, height, data.getBorder(), + data.getPixelFormat(), data.getPixelType(), null); + updateSubImageImpl(gl, data, texTarget, i, 0, 0, 0, 0, data.getWidth(), data.getHeight()); + + width = Math.max(width / 2, 1); + height = Math.max(height / 2, 1); + } + } else { + if (data.getMipmap()) { + // For now, only use hardware mipmapping for uncompressed 2D + // textures where the user hasn't explicitly specified + // mipmap data; don't know about interactions between + // GL_GENERATE_MIPMAP and glCompressedTexImage2D + gl.glTexParameteri(texParamTarget, gl.GL_GENERATE_MIPMAP(), gl.GL_TRUE()); + usingAutoMipmapGeneration = true; + } + + gl.glTexImage2D(texTarget, 0, data.getInternalFormat(), + texWidth, texHeight, data.getBorder(), + data.getPixelFormat(), data.getPixelType(), null); + updateSubImageImpl(gl, data, texTarget, 0, 0, 0, 0, 0, data.getWidth(), data.getHeight()); + } + + final int minFilter = (data.getMipmap() ? gl.GL_LINEAR_MIPMAP_LINEAR() : gl.GL_LINEAR()); + final int magFilter = gl.GL_LINEAR(); + final int wrapMode = gl.GL_CLAMP_TO_EDGE(); + + gl.glTexParameteri(texParamTarget, gl.GL_TEXTURE_MIN_FILTER(), minFilter); + gl.glTexParameteri(texParamTarget, gl.GL_TEXTURE_MAG_FILTER(), magFilter); + gl.glTexParameteri(texParamTarget, gl.GL_TEXTURE_WRAP_S(), wrapMode); + gl.glTexParameteri(texParamTarget, gl.GL_TEXTURE_WRAP_T(), wrapMode); + if (this.target == gl.GL_TEXTURE_CUBE_MAP()) { + gl.glTexParameteri(texParamTarget, gl.GL_TEXTURE_WRAP_R(), wrapMode); + } + + // Don't overwrite target if we're loading e.g. faces of a cube + // map + if ((this.target == 0) || + (this.target == gl.GL_TEXTURE_2D())) { + this.target = texTarget; + } + + } + + /** + * Updates a subregion of the content area of this texture using the + * given data. If automatic mipmap generation is in use (see {@link + * #isUsingAutoMipmapGeneration isUsingAutoMipmapGeneration}), + * updates to the base (level 0) mipmap will cause the lower-level + * mipmaps to be regenerated, and updates to other mipmap levels + * will be ignored. Otherwise, if automatic mipmap generation is not + * in use, only updates the specified mipmap level and does not + * re-generate mipmaps if they were originally produced or loaded. + * + * @param data the image data to be uploaded to this texture + * @param mipmapLevel the mipmap level of the texture to set. If + * this is non-zero and the TextureData contains mipmap data, the + * appropriate mipmap level will be selected. + * @param x the x offset (in pixels) relative to the lower-left corner + * of this texture + * @param y the y offset (in pixels) relative to the lower-left corner + * of this texture + * + + */ + public void updateSubImage(final GL gl, final TextureData data, final int mipmapLevel, final int x, final int y) { + if (usingAutoMipmapGeneration && mipmapLevel != 0) { + // When we're using mipmap generation via GL_GENERATE_MIPMAP, we + // don't need to update other mipmap levels + return; + } + bind(gl); + updateSubImageImpl(gl, data, target, mipmapLevel, x, y, 0, 0, data.getWidth(), data.getHeight()); + } + + /** + * Updates a subregion of the content area of this texture using the + * specified sub-region of the given data. If automatic mipmap + * generation is in use (see {@link #isUsingAutoMipmapGeneration + * isUsingAutoMipmapGeneration}), updates to the base (level 0) + * mipmap will cause the lower-level mipmaps to be regenerated, and + * updates to other mipmap levels will be ignored. Otherwise, if + * automatic mipmap generation is not in use, only updates the + * specified mipmap level and does not re-generate mipmaps if they + * were originally produced or loaded. This method is only supported + * for uncompressed TextureData sources. + * + * @param data the image data to be uploaded to this texture + * @param mipmapLevel the mipmap level of the texture to set. If + * this is non-zero and the TextureData contains mipmap data, the + * appropriate mipmap level will be selected. + * @param dstx the x offset (in pixels) relative to the lower-left corner + * of this texture where the update will be applied + * @param dsty the y offset (in pixels) relative to the lower-left corner + * of this texture where the update will be applied + * @param srcx the x offset (in pixels) relative to the lower-left corner + * of the supplied TextureData from which to fetch the update rectangle + * @param srcy the y offset (in pixels) relative to the lower-left corner + * of the supplied TextureData from which to fetch the update rectangle + * @param width the width (in pixels) of the rectangle to be updated + * @param height the height (in pixels) of the rectangle to be updated + * + + * OpenGL-related errors occurred + */ + public void updateSubImage(final GL gl, final TextureData data, final int mipmapLevel, + final int dstx, final int dsty, + final int srcx, final int srcy, + final int width, final int height) { + if (false) { + throw gl.newGLException("updateSubImage specifying a sub-rectangle is not supported for compressed TextureData"); + } + if (usingAutoMipmapGeneration && mipmapLevel != 0) { + // When we're using mipmap generation via GL_GENERATE_MIPMAP, we + // don't need to update other mipmap levels + return; + } + bind(gl); + updateSubImageImpl(gl, data, target, mipmapLevel, dstx, dsty, srcx, srcy, width, height); + } + + /** + * Sets the OpenGL floating-point texture parameter for the + * texture's target. This gives control over parameters such as + * GL_TEXTURE_MAX_ANISOTROPY_EXT. Causes this texture to be bound to + * the current texture state. + * + + * OpenGL-related errors occurred + */ + public void setTexParameterf(final GL gl, final int parameterName, + final float value) { + bind(gl); + gl.glTexParameterf(target, parameterName, value); + } + + /** + * Sets the OpenGL integer texture parameter for the texture's + * target. This gives control over parameters such as + * GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T, which by default are set + * to GL_CLAMP_TO_EDGE if OpenGL 1.2 is supported on the current + * platform and GL_CLAMP if not. Causes this texture to be bound to + * the current texture state. + * + + */ + public void setTexParameteri(final GL gl, final int parameterName, + final int value) { + bind(gl); + gl.glTexParameteri(target, parameterName, value); + } + + + /** + * Returns the underlying OpenGL texture object for this texture + * and generates it if not done yet. + *

+ * Most applications will not need to access this, since it is + * handled automatically by the bind(GL) and destroy(GL) APIs. + *

+ * @param gl required to be valid and current in case the texture object has not been generated yet, + * otherwise it may be null. + * @see #getTextureObject() + */ + public int getTextureObject(final GL gl) { + validateTexID(gl, false); + return texID; + } + + /** + * Returns the underlying OpenGL texture object for this texture, + * maybe 0 if not yet generated. + *

+ * Most applications will not need to access this, since it is + * handled automatically by the bind(GL) and destroy(GL) APIs. + *

+ * @see #getTextureObject(GL) + */ + public int getTextureObject() { + return texID; + } + + /** Indicates whether this Texture is using automatic mipmap + generation (via the OpenGL texture parameter + GL_GENERATE_MIPMAP). This will automatically be used when + mipmapping is requested via the TextureData and either OpenGL + 1.4 or the GL_SGIS_generate_mipmap extension is available. If + so, updates to the base image (mipmap level 0) will + automatically propagate down to the lower mipmap levels. Manual + updates of the mipmap data at these lower levels will be + ignored. */ + public boolean isUsingAutoMipmapGeneration() { + return usingAutoMipmapGeneration; + } + + //---------------------------------------------------------------------- + // Internals only below this point + // + + private void updateTexCoords() { + if (mustFlipVertically) { + coords = new TextureCoords(0, // l + (float) imgHeight / (float) texHeight, // b + (float) imgWidth / (float) texWidth, // r + 0 // t + ); + } else { + coords = new TextureCoords(0, // l + 0, // b + (float) imgWidth / (float) texWidth, // r + (float) imgHeight / (float) texHeight // t + ); + } + } + + private void updateSubImageImpl(final GL gl, final TextureData data, final int newTarget, final int mipmapLevel, + int dstx, int dsty, + int srcx, int srcy, int width, int height) { + ByteBuffer buffer = data.getBuffer(); + if (buffer == null && data.getMipmapData() == null) { + // Assume user just wanted to get the Texture object allocated + return; + } + + int rowlen = data.getRowLength(); + int dataWidth = data.getWidth(); + int dataHeight = data.getHeight(); + if (data.getMipmapData() != null) { + // Compute the width, height and row length at the specified mipmap level + // Note we do not support specification of the row length for + // mipmapped textures at this point + for (int i = 0; i < mipmapLevel; i++) { + width = Math.max(width / 2, 1); + height = Math.max(height / 2, 1); + + dataWidth = Math.max(dataWidth / 2, 1); + dataHeight = Math.max(dataHeight / 2, 1); + } + rowlen = 0; + buffer = data.getMipmapData()[mipmapLevel]; + } + + // Clip incoming rectangles to what is available both on this + // texture and in the incoming TextureData + if (srcx < 0) { + width += srcx; + srcx = 0; + } + if (srcy < 0) { + height += srcy; + srcy = 0; + } + // NOTE: not sure whether the following two are the correct thing to do + if (dstx < 0) { + width += dstx; + dstx = 0; + } + if (dsty < 0) { + height += dsty; + dsty = 0; + } + + if (srcx + width > dataWidth) { + width = dataWidth - srcx; + } + if (srcy + height > dataHeight) { + height = dataHeight - srcy; + } + if (dstx + width > texWidth) { + width = texWidth - dstx; + } + if (dsty + height > texHeight) { + height = texHeight - dsty; + } + + checkCompressedTextureExtensions(gl, data); + + final int[] align = { 0 }; + final int[] rowLength = { 0 }; + final int[] skipRows = { 0 }; + final int[] skipPixels = { 0 }; + gl.glGetIntegerv(gl.GL_UNPACK_ALIGNMENT(), align); // save alignment + gl.glGetIntegerv(gl.GL_UNPACK_ROW_LENGTH(), rowLength); // save row length + gl.glGetIntegerv(gl.GL_UNPACK_SKIP_ROWS(), skipRows); // save skipped rows + gl.glGetIntegerv(gl.GL_UNPACK_SKIP_PIXELS(), skipPixels); // save skipped pixels + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT(), data.getAlignment()); + gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH(), rowlen); + gl.glPixelStorei(gl.GL_UNPACK_SKIP_ROWS(), srcy); + gl.glPixelStorei(gl.GL_UNPACK_SKIP_PIXELS(), srcx); + + gl.glTexSubImage2D(newTarget, mipmapLevel, + dstx, dsty, width, height, + data.getPixelFormat(), data.getPixelType(), + buffer); + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT(), align[0]); // restore alignment + gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH(), rowLength[0]); // restore row length + gl.glPixelStorei(gl.GL_UNPACK_SKIP_ROWS(), skipRows[0]); // restore skipped rows + gl.glPixelStorei(gl.GL_UNPACK_SKIP_PIXELS(), skipPixels[0]); // restore skipped pixels + } + + private void checkCompressedTextureExtensions(final GL gl, final TextureData data) { + } + + private boolean validateTexID(final GL gl, final boolean throwException) { + if( 0 == texID ) { + if( null != gl ) { + final int[] tmp = new int[1]; + gl.glGenTextures(tmp); + texID = tmp[0]; + if ( 0 == texID && throwException ) { + throw gl.newGLException("Create texture ID invalid: texID "+texID+", glerr 0x"+Integer.toHexString(gl.glGetError())); + } + } else if ( throwException ) { + throw gl.newGLException("No GL context given, can't create texture ID"); + } + } + return 0 != texID; + } + + // Helper routines for disabling certain codepaths + +} diff --git a/src/main/java/net/opengrabeso/opengl/util/texture/TextureCoords.java b/src/main/java/net/opengrabeso/opengl/util/texture/TextureCoords.java new file mode 100644 index 00000000..ac01c23f --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/texture/TextureCoords.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. + * Copyright (c) 2012 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + */ + +package net.opengrabeso.opengl.util.texture; + +/** Specifies texture coordinates for a rectangular area of a + texture. Note that some textures are inherently flipped vertically + from OpenGL's standard coordinate system. This class takes care of + this vertical flip so that the "bottom" and "top" coordinates may + sometimes be reversed. From the point of view of code rendering + textured polygons, it can always map the bottom and left texture + coordinates from the TextureCoords to the lower left point of the + textured polygon and achieve correct results. */ + +public class TextureCoords { + // These represent the lower-left point + private final float left; + private final float bottom; + // These represent the upper-right point + private final float right; + private final float top; + + public TextureCoords(final float left, final float bottom, + final float right, final float top) { + this.left = left; + this.bottom = bottom; + this.right = right; + this.top = top; + } + + /** Transfers {s * ss, t * ts} from this object into the given float[8+d_off] in the following order: + *
+     *   left,  bottom
+     *   right, bottom
+     *   left,  top
+     *   right  top
+     * 
+ */ + public float[] getST_LB_RB_LT_RT(final float[] d, final int d_off, final float ss, final float ts) { + d[0+d_off] = left *ss; d[1+d_off] = bottom*ts; + d[2+d_off] = right *ss; d[3+d_off] = bottom*ts; + d[4+d_off] = left *ss; d[5+d_off] = top *ts; + d[6+d_off] = right *ss; d[7+d_off] = top *ts; + return d; + } + + /** Returns the leftmost (x) texture coordinate of this + rectangle. */ + public float left() { return left; } + + /** Returns the rightmost (x) texture coordinate of this + rectangle. */ + public float right() { return right; } + + /** Returns the bottommost (y) texture coordinate of this + rectangle. */ + public float bottom() { return bottom; } + + /** Returns the topmost (y) texture coordinate of this + rectangle. */ + public float top() { return top; } + + @Override + public String toString() { return "TexCoord[h: "+left+" - "+right+", v: "+bottom+" - "+top+"]"; } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/texture/TextureData.java b/src/main/java/net/opengrabeso/opengl/util/texture/TextureData.java new file mode 100644 index 00000000..5b716637 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/texture/TextureData.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. + * Copyright (c) 2010 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + */ + +package net.opengrabeso.opengl.util.texture; + +import java.nio.ByteBuffer; + +import net.opengrabeso.opengl.util.GLPixelAttributes; + +/** + * Represents the data for an OpenGL texture. This is separated from + * the notion of a Texture to support things like streaming in of + * textures in a background thread without requiring an OpenGL context + * to be current on that thread. + * + * @author Chris Campbell + * @author Kenneth Russell + * @author Sven Gothel + */ + +public class TextureData { + protected int width; + protected int height; + private int border; + protected GLPixelAttributes pixelAttributes; + protected int internalFormat; // perhaps inferred from pixelFormat? + protected boolean mipmap; // indicates whether mipmaps should be generated + protected boolean mustFlipVertically; // Must flip texture coordinates + // vertically to get OpenGL output + // to look correct + protected ByteBuffer buffer; // the actual data... + private ByteBuffer[] mipmapData; // ...or a series of mipmaps + private Flusher flusher; + protected int rowLength; + protected int alignment; // 1, 2, or 4 bytes + + /** Used only by subclasses */ + protected TextureData() { this.pixelAttributes = GLPixelAttributes.UNDEF; } + + public TextureData(final int internalFormat, + final int width, + final int height, + final int border, + final int dataFormat, + final int dataType, + final boolean mipmap, + final boolean dataIsCompressed, + final boolean mustFlipVertically, + final ByteBuffer buffer, + final Flusher flusher) throws IllegalArgumentException { + if (mipmap && dataIsCompressed) { + throw new IllegalArgumentException("Can not generate mipmaps for compressed textures"); + } + + this.width = width; + this.height = height; + this.border = border; + this.pixelAttributes = new GLPixelAttributes(dataFormat, dataType); + this.internalFormat = internalFormat; + this.mipmap = mipmap; + this.mustFlipVertically = mustFlipVertically; + this.buffer = buffer; + this.flusher = flusher; + alignment = 1; // FIXME: is this correct enough in all situations? + } + + /** Returns the width in pixels of the texture data. */ + public int getWidth() { return width; } + /** Returns the height in pixels of the texture data. */ + public int getHeight() { return height; } + /** Returns the border in pixels of the texture data. */ + public int getBorder() { + return border; + } + /** Returns the intended OpenGL {@link GLPixelAttributes} of the texture data, i.e. format and type. */ + public GLPixelAttributes getPixelAttributes() { + return pixelAttributes; + } + /** Returns the intended OpenGL pixel format of the texture data using {@link #getPixelAttributes()}. */ + public int getPixelFormat() { + return pixelAttributes.format; + } + /** Returns the intended OpenGL pixel type of the texture data using {@link #getPixelAttributes()}. */ + public int getPixelType() { + return pixelAttributes.type; + } + /** Returns the intended OpenGL internal format of the texture data. */ + public int getInternalFormat() { + return internalFormat; + } + /** Returns whether mipmaps should be generated for the texture data. */ + public boolean getMipmap() { + return mipmap; + } + + /** Indicates whether the texture coordinates must be flipped + vertically for proper display. */ + public boolean getMustFlipVertically() { + return mustFlipVertically; + } + /** Returns the texture data, or null if it is specified as a set of mipmaps. */ + public ByteBuffer getBuffer() { + return buffer; + } + /** Returns all mipmap levels for the texture data, or null if it is + specified as a single image. */ + public ByteBuffer[] getMipmapData() { + return mipmapData; + } + /** Returns the required byte alignment for the texture data. */ + public int getAlignment() { + return alignment; + } + /** Returns the row length needed for correct GL_UNPACK_ROW_LENGTH + specification. This is currently only supported for + non-mipmapped, non-compressed textures. */ + public int getRowLength() { + return rowLength; + } + + /** Sets the width in pixels of the texture data. */ + public void setWidth(final int width) { this.width = width; } + /** Sets the height in pixels of the texture data. */ + public void setHeight(final int height) { this.height = height; } + /** Sets the border in pixels of the texture data. */ + public void setBorder(final int border) { this.border = border; } + /** Sets the intended OpenGL pixel format of the texture data. */ + public void setPixelAttributes(final GLPixelAttributes pixelAttributes) { this.pixelAttributes = pixelAttributes; } + /** + * Sets the intended OpenGL pixel format component of {@link GLPixelAttributes} of the texture data. + *

+ * Use {@link #setPixelAttributes(GLPixelAttributes)}, if setting format and type. + *

+ */ + public void setPixelFormat(final int pixelFormat) { + if( pixelAttributes.format != pixelFormat ) { + pixelAttributes = new GLPixelAttributes(pixelFormat, pixelAttributes.type); + } + } + /** + * Sets the intended OpenGL pixel type component of {@link GLPixelAttributes} of the texture data. + *

+ * Use {@link #setPixelAttributes(GLPixelAttributes)}, if setting format and type. + *

+ */ + public void setPixelType(final int pixelType) { + if( pixelAttributes.type != pixelType) { + pixelAttributes = new GLPixelAttributes(pixelAttributes.format, pixelType); + } + } + /** Sets the intended OpenGL internal format of the texture data. */ + public void setInternalFormat(final int internalFormat) { this.internalFormat = internalFormat; } + /** Sets whether mipmaps should be generated for the texture data. */ + public void setMipmap(final boolean mipmap) { this.mipmap = mipmap; } + /** Sets whether the texture data is in compressed form. */ + public void setIsDataCompressed(final boolean compressed) { + } + /** Sets whether the texture coordinates must be flipped vertically + for proper display. */ + public void setMustFlipVertically(final boolean mustFlipVertically) { this.mustFlipVertically = mustFlipVertically; } + /** Sets the texture data. */ + public void setBuffer(final ByteBuffer buffer) { + this.buffer = buffer; + } + /** Sets the required byte alignment for the texture data. */ + public void setAlignment(final int alignment) { this.alignment = alignment; } + /** Sets the row length needed for correct GL_UNPACK_ROW_LENGTH + specification. This is currently only supported for + non-mipmapped, non-compressed textures. */ + public void setRowLength(final int rowLength) { this.rowLength = rowLength; } + + /** Flushes resources associated with this TextureData by calling + Flusher.flush(). */ + public void flush() { + if (flusher != null) { + flusher.flush(); + flusher = null; + } + } + + /** Calls flush() + * @see #flush() + */ + public void destroy() { + flush(); + } + + /** Defines a callback mechanism to allow the user to explicitly + deallocate native resources (memory-mapped files, etc.) + associated with a particular TextureData. */ + public static interface Flusher { + /** Flushes any native resources associated with this + TextureData. */ + public void flush(); + } + + @Override + public String toString() { + return "TextureData["+width+"x"+height+", y-flip "+mustFlipVertically+", internFormat 0x"+Integer.toHexString(internalFormat)+", "+ + pixelAttributes+", border "+border+", alignment "+alignment+", rowlen "+rowLength+ ""; + } + +} diff --git a/src/main/java/net/opengrabeso/opengl/util/texture/TextureState.java b/src/main/java/net/opengrabeso/opengl/util/texture/TextureState.java new file mode 100644 index 00000000..8873c913 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/texture/TextureState.java @@ -0,0 +1,165 @@ +/** + * Copyright 2013 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package net.opengrabeso.opengl.util.texture; + +import com.jogamp.opengl.*; + + +/** + * Preserves a [ texture-unit, texture-target ] state. + *

+ * The states keys are the retrieved active texture-unit and the given texture-target + * for which the following states are being queried: + *

+ *   - texture-object
+ *   - GL.GL_TEXTURE_MAG_FILTER
+ *   - GL.GL_TEXTURE_MIN_FILTER
+ *   - GL.GL_TEXTURE_WRAP_S
+ *   - GL.GL_TEXTURE_WRAP_T
+ * 
+ */ +public class TextureState { + /** + * Returns the pname to query the textureTarget currently bound to the active texture-unit. + *

+ * Returns 0 is textureTarget is not supported. + *

+ */ + public static final int getTextureTargetQueryName(final int textureTarget) { + final int texBindQName; + switch(textureTarget) { + case GL.GL_TEXTURE_2D: texBindQName = GL.GL_TEXTURE_BINDING_2D; break; + case GL.GL_TEXTURE_CUBE_MAP: texBindQName = GL.GL_TEXTURE_BINDING_CUBE_MAP; break; + case GL2ES2.GL_TEXTURE_3D: texBindQName = GL2ES2.GL_TEXTURE_BINDING_3D; break; + case GL2GL3.GL_TEXTURE_1D: texBindQName = GL2GL3.GL_TEXTURE_BINDING_1D; break; + case GL2GL3.GL_TEXTURE_1D_ARRAY: texBindQName = GL2GL3.GL_TEXTURE_BINDING_1D_ARRAY; break; + case GL2ES3.GL_TEXTURE_2D_ARRAY: texBindQName = GL2ES3.GL_TEXTURE_BINDING_2D_ARRAY; break; + case GL2GL3.GL_TEXTURE_RECTANGLE: texBindQName = GL2GL3.GL_TEXTURE_BINDING_RECTANGLE; break; + case GL2GL3.GL_TEXTURE_BUFFER: texBindQName = GL2GL3.GL_TEXTURE_BINDING_BUFFER; break; + case GL2ES2.GL_TEXTURE_2D_MULTISAMPLE: texBindQName = GL2ES2.GL_TEXTURE_BINDING_2D_MULTISAMPLE; break; + case GL2ES2.GL_TEXTURE_2D_MULTISAMPLE_ARRAY: texBindQName = GL2ES2.GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY; break; + default: texBindQName = 0; + } + return texBindQName; + } + + private final int target; + /** + *
+     *   0 - unit
+     *   1 - texture object
+     *   2 - GL.GL_TEXTURE_MAG_FILTER
+     *   3 - GL.GL_TEXTURE_MIN_FILTER
+     *   4 - GL.GL_TEXTURE_WRAP_S
+     *   5 - GL.GL_TEXTURE_WRAP_T
+     * 
+ */ + private final int[] state = new int[] { 0, 0, 0, 0, 0, 0 }; + + private static final String toHexString(final int i) { return "0x"+Integer.toHexString(i); } + + private static final int activeTexture(final GL gl) { + final int[] vi = { 0 }; + gl.glGetIntegerv(GL.GL_ACTIVE_TEXTURE, vi, 0); + return vi[0]; + } + + /** + * Creates a texture state for the retrieved active texture-unit and the given texture-target. + * See {@link TextureState}. + * @param gl current GL context's GL object + * @param textureTarget + + */ + public TextureState(final GL gl, final int textureTarget) { + this(gl, activeTexture(gl), textureTarget); + } + + /** + * Creates a texture state for the given active texture-unit and the given texture-target. + * See {@link TextureState}. + * @param gl current GL context's GL object + * @param textureUnit of range [ {@link GL#GL_TEXTURE0}.. ] + * @param textureTarget + + */ + public TextureState(final GL gl, final int textureUnit, final int textureTarget) { + target = textureTarget; + state[0] = textureUnit; + final int texBindQName = getTextureTargetQueryName(textureTarget); + if( 0 == texBindQName ) { + throw new GLException("Unsupported textureTarget "+toHexString(textureTarget)); + } + gl.glGetIntegerv(texBindQName, state, 1); + gl.glGetTexParameteriv(target, GL.GL_TEXTURE_MAG_FILTER, state, 2); + gl.glGetTexParameteriv(target, GL.GL_TEXTURE_MIN_FILTER, state, 3); + gl.glGetTexParameteriv(target, GL.GL_TEXTURE_WRAP_S, state, 4); + gl.glGetTexParameteriv(target, GL.GL_TEXTURE_WRAP_T, state, 5); + } + + /** + * Restores the texture-unit's texture-target state. + *

+ * First the texture-unit is activated, then all states are restored. + *

+ * @param gl current GL context's GL object + */ + public final void restore(final GL gl) { + gl.glActiveTexture(state[0]); + gl.glBindTexture(target, state[1]); + gl.glTexParameteri(target, GL.GL_TEXTURE_MAG_FILTER, state[2]); + gl.glTexParameteri(target, GL.GL_TEXTURE_MIN_FILTER, state[3]); + gl.glTexParameteri(target, GL.GL_TEXTURE_WRAP_S, state[4]); + gl.glTexParameteri(target, GL.GL_TEXTURE_WRAP_T, state[5]); + } + + /** Returns the texture-unit of this state, key value. Unit is of range [ {@link GL#GL_TEXTURE0}.. ]. */ + public final int getUnit() { return state[0]; } + /** Returns the texture-target of this state, key value. */ + public final int getTarget() { return target; } + + /** Returns the state's texture-object. */ + public final int getObject() { return state[1]; } + /** Returns the state's mag-filter param. */ + public final int getMagFilter() { return state[2]; } + /** Returns the state's min-filter param. */ + public final int getMinFilter() { return state[3]; } + /** Returns the state's wrap-s param. */ + public final int getWrapS() { return state[4]; } + /** Returns the state's wrap-t param. */ + public final int getWrapT() { return state[5]; } + + + @Override + public final String toString() { + return "TextureState[unit "+(state[0] - GL.GL_TEXTURE0)+", target "+toHexString(target)+ + ": obj "+toHexString(state[1])+ + ", filter[mag "+toHexString(state[2])+", min "+toHexString(state[3])+"], "+ + ": wrap[s "+toHexString(state[4])+", t "+toHexString(state[5])+"]]"; + } +} diff --git a/src/main/java/net/opengrabeso/opengl/util/texture/awt/AWTTextureData.java b/src/main/java/net/opengrabeso/opengl/util/texture/awt/AWTTextureData.java new file mode 100644 index 00000000..9689b775 --- /dev/null +++ b/src/main/java/net/opengrabeso/opengl/util/texture/awt/AWTTextureData.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. + * Copyright (c) 2010 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR + * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE + * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, + * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF + * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for use + * in the design, construction, operation or maintenance of any nuclear + * facility. + */ + +package net.opengrabeso.opengl.util.texture.awt; + +import java.awt.image.BufferedImage; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.nio.ByteBuffer; + +import com.github.opengrabeso.jaagl.GL2; +import net.opengrabeso.opengl.util.GLPixelAttributes; +import net.opengrabeso.opengl.util.texture.TextureData; + +public class AWTTextureData extends TextureData { + + /** + * Constructs a new TextureData object with the specified parameters + * and data contained in the given BufferedImage. The resulting + * TextureData "wraps" the contents of the BufferedImage, so if a + * modification is made to the BufferedImage between the time the + * TextureData is constructed and when a Texture is made from the + * TextureData, that modification will be visible in the resulting + * Texture. + * + * @param internalFormat the OpenGL internal format for the + * resulting texture; may be 0, in which case + * it is inferred from the image's type + * @param mipmap indicates whether mipmaps should be + * autogenerated (using GLU) for the resulting + * texture + * @param image the image containing the texture data + */ + public AWTTextureData(final GL2 gl, + int internalFormat, + final boolean mipmap, + final BufferedImage image) { + assert internalFormat != 0; + this.internalFormat = internalFormat; + createFromImage(gl, image); + this.mipmap = mipmap; + } + + private void validatePixelAttributes() { + } + + @Override + public GLPixelAttributes getPixelAttributes() { + validatePixelAttributes(); + return super.getPixelAttributes(); + } + + @Override + public int getPixelFormat() { + validatePixelAttributes(); + return super.getPixelFormat(); + } + @Override + public int getPixelType() { + validatePixelAttributes(); + return super.getPixelType(); + } + + @Override + public ByteBuffer getBuffer() { + return buffer; + } + + private void createFromImage(GL2 gl, final BufferedImage image) { + pixelAttributes = GLPixelAttributes.UNDEF; // Determine from image + mustFlipVertically = true; + + width = image.getWidth(); + height = image.getHeight(); + + int scanlineStride; + + final SampleModel sm = image.getRaster().getSampleModel(); + if (sm instanceof SinglePixelPackedSampleModel) { + scanlineStride = + ((SinglePixelPackedSampleModel)sm).getScanlineStride(); + } else if (sm instanceof MultiPixelPackedSampleModel) { + scanlineStride = + ((MultiPixelPackedSampleModel)sm).getScanlineStride(); + } else if (sm instanceof ComponentSampleModel) { + scanlineStride = + ((ComponentSampleModel)sm).getScanlineStride(); + } else { + throw gl.newGLException("Unexpected sample model"); + } + + width = image.getWidth(); + height = image.getHeight(); + + switch (image.getType()) { + case BufferedImage.TYPE_BYTE_GRAY: + pixelAttributes = new GLPixelAttributes(gl.GL_LUMINANCE(), gl.GL_UNSIGNED_BYTE()); + rowLength = scanlineStride; + alignment = 1; + break; + default: + throw gl.newGLException("Unsupported image type"); + } + + createNIOBufferFromImage(image); + } + + private boolean isPackedInt(final BufferedImage image) { + final int imgType = image.getType(); + return imgType == BufferedImage.TYPE_INT_RGB || + imgType == BufferedImage.TYPE_INT_BGR || + imgType == BufferedImage.TYPE_INT_ARGB || + imgType == BufferedImage.TYPE_INT_ARGB_PRE; + } + + private void createNIOBufferFromImage(final BufferedImage image) { + buffer = wrapImageDataBuffer(image); + } + + private ByteBuffer wrapImageDataBuffer(final BufferedImage image) { + // + // Note: Grabbing the DataBuffer will defeat Java2D's image + // management mechanism (as of JDK 5/6, at least). This shouldn't + // be a problem for most JOGL apps, but those that try to upload + // the image into an OpenGL texture and then use the same image in + // Java2D rendering might find the 2D rendering is not as fast as + // it could be. + // + + final DataBuffer data = image.getRaster().getDataBuffer(); + if (data instanceof DataBufferByte) { + return ByteBuffer.wrap(((DataBufferByte) data).getData()); + } else { + throw new RuntimeException("Unexpected DataBuffer type?"); + } + } +} diff --git a/src/test/java/net/opengrabeso/opengl/text/Issue344Base.java b/src/test/java/net/opengrabeso/opengl/text/Issue344Base.java new file mode 100644 index 00000000..825fea41 --- /dev/null +++ b/src/test/java/net/opengrabeso/opengl/text/Issue344Base.java @@ -0,0 +1,122 @@ +package net.opengrabeso.opengl.text; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.Frame; +import java.awt.event.*; +import java.awt.geom.*; + +import com.github.opengrabeso.jaagl.jogl.JoGL; +import com.jogamp.common.util.InterruptSource; +import com.jogamp.opengl.GL; +import com.jogamp.opengl.GL2; +import com.jogamp.opengl.GLAutoDrawable; +import com.jogamp.opengl.GLEventListener; +import com.jogamp.opengl.awt.GLCanvas; +import com.jogamp.opengl.fixedfunc.GLMatrixFunc; +import com.jogamp.opengl.glu.*; +import net.opengrabeso.opengl.util.awt.TextRenderer; + +/** Test Code adapted from TextCube.java (in JOGL demos) + * + * @author spiraljetty + * @author kbr + */ + +public abstract class Issue344Base implements GLEventListener +{ + GLU glu = new GLU(); + TextRenderer renderer; + + float textScaleFactor; + Font font; + boolean useMipMaps; + + protected Issue344Base() { + font = new Font("default", Font.PLAIN, 200); + useMipMaps = true; //false + } + + protected abstract String getText(); + + protected void run(final String[] args) { + final Frame frame = new Frame(getClass().getName()); + frame.setLayout(new BorderLayout()); + + final GLCanvas canvas = new GLCanvas(); + canvas.addGLEventListener(this); + frame.add(canvas, BorderLayout.CENTER); + + frame.setSize(512, 512); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(final WindowEvent e) { + new InterruptSource.Thread(null, new Runnable() { + public void run() { + System.exit(0); + } + }).start(); + } + }); + try { + javax.swing.SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + frame.setVisible(true); + } } ); + } catch(final Exception ex) { + throw new RuntimeException(ex); + } + } + + public void init(final GLAutoDrawable drawable) + { + final GL2 jgl = drawable.getGL().getGL2(); + + final com.github.opengrabeso.jaagl.GL2 gl = JoGL.wrap(jgl); + + gl.glEnable(GL.GL_DEPTH_TEST); + + renderer = new TextRenderer(gl, font, useMipMaps); + + final Rectangle2D bounds = renderer.getBounds(getText()); + final float w = (float) bounds.getWidth(); + // final float h = (float) bounds.getHeight(); + textScaleFactor = 2.0f / (w * 1.1f); + //gl.setSwapInterval(0); + } + + public void display(final GLAutoDrawable drawable) + { + final GL2 jgl = drawable.getGL().getGL2(); + + final com.github.opengrabeso.jaagl.GL2 gl = JoGL.wrap(jgl); + gl.glClear(gl.GL_COLOR_BUFFER_BIT() | gl.GL_DEPTH_BUFFER_BIT()); + + gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); + gl.glLoadIdentity(); + glu.gluLookAt(0, 0, 10, + 0, 0, 0, + 0, 1, 0); + + renderer.begin3DRendering(); + final Rectangle2D bounds = renderer.getBounds(getText()); + final float w = (float) bounds.getWidth(); + final float h = (float) bounds.getHeight(); + renderer.draw3D(getText(), + w / -2.0f * textScaleFactor, + h / -2.0f * textScaleFactor, + 3f, + textScaleFactor); + + renderer.end3DRendering(); + } + + public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) + { + final GL2 gl = drawable.getGL().getGL2(); + gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION); + gl.glLoadIdentity(); + glu.gluPerspective(15, (float) width / (float) height, 5, 15); + } + + public void dispose(final GLAutoDrawable drawable) {} +} diff --git a/src/test/java/net/opengrabeso/opengl/text/Issue344Test1.java b/src/test/java/net/opengrabeso/opengl/text/Issue344Test1.java new file mode 100644 index 00000000..d705b7f9 --- /dev/null +++ b/src/test/java/net/opengrabeso/opengl/text/Issue344Test1.java @@ -0,0 +1,12 @@ +package net.opengrabeso.opengl.text; + +public class Issue344Test1 extends Issue344Base { + protected String getText() { + // test 1 - weird artifacts appear with a large font & long string + return "abcdefghijklmnopqrstuvwxyz1234567890"; + } + + public static void main(final String[] args) { + new Issue344Test1().run(args); + } +} diff --git a/src/test/java/net/opengrabeso/opengl/text/Issue344Test2.java b/src/test/java/net/opengrabeso/opengl/text/Issue344Test2.java new file mode 100644 index 00000000..d6ebd5b8 --- /dev/null +++ b/src/test/java/net/opengrabeso/opengl/text/Issue344Test2.java @@ -0,0 +1,12 @@ +package net.opengrabeso.opengl.text; + +public class Issue344Test2 extends Issue344Base { + protected String getText() { + // test 2 - unicode hangs program with a large font & long string + return "\u201Cabcdefghijklmnopqrstuvwxyz\u201D"; + } + + public static void main(final String[] args) { + new Issue344Test2().run(args); + } +} diff --git a/src/test/java/net/opengrabeso/opengl/text/Issue344Test3.java b/src/test/java/net/opengrabeso/opengl/text/Issue344Test3.java new file mode 100644 index 00000000..d2b46946 --- /dev/null +++ b/src/test/java/net/opengrabeso/opengl/text/Issue344Test3.java @@ -0,0 +1,12 @@ +package net.opengrabeso.opengl.text; + +public class Issue344Test3 extends Issue344Base { + protected String getText() { + // test 3 - slight rendering artifacts around very large letters + return "abcde"; + } + + public static void main(final String[] args) { + new Issue344Test3().run(args); + } +} diff --git a/src/test/java/net/opengrabeso/opengl/text/Issue344Test4.java b/src/test/java/net/opengrabeso/opengl/text/Issue344Test4.java new file mode 100644 index 00000000..a6b1af82 --- /dev/null +++ b/src/test/java/net/opengrabeso/opengl/text/Issue344Test4.java @@ -0,0 +1,12 @@ +package net.opengrabeso.opengl.text; + +public class Issue344Test4 extends Issue344Base { + protected String getText() { + // test 4 - unicode letter as second-to-last is rendered incorrectly + return "\u201CGreetings\u201D!"; + } + + public static void main(final String[] args) { + new Issue344Test4().run(args); + } +}