/* * Copyright (c) 2008-2012, Matthias Mann * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 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. * * Neither the name of Matthias Mann nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "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 THE COPYRIGHT OWNER 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. */ package de.matthiasmann.twl.renderer.lwjgl; import de.matthiasmann.twl.Color; import de.matthiasmann.twl.Event; import de.matthiasmann.twl.Rect; import de.matthiasmann.twl.renderer.AnimationState; import de.matthiasmann.twl.renderer.AnimationState.StateKey; import de.matthiasmann.twl.renderer.CacheContext; import de.matthiasmann.twl.renderer.DynamicImage; import de.matthiasmann.twl.renderer.FontParameter; import de.matthiasmann.twl.renderer.Gradient; import de.matthiasmann.twl.renderer.Image; import de.matthiasmann.twl.renderer.MouseCursor; import de.matthiasmann.twl.renderer.Font; import de.matthiasmann.twl.renderer.FontMapper; import de.matthiasmann.twl.renderer.LineRenderer; import de.matthiasmann.twl.renderer.OffscreenRenderer; import de.matthiasmann.twl.renderer.Renderer; import de.matthiasmann.twl.renderer.Texture; import de.matthiasmann.twl.utils.ClipStack; import de.matthiasmann.twl.utils.StateSelect; import java.io.IOException; import java.net.URL; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.util.ArrayList; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import org.lwjgl.BufferUtils; import org.lwjgl.LWJGLException; import org.lwjgl.Sys; import org.lwjgl.input.Cursor; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.ContextCapabilities; import org.lwjgl.opengl.EXTTextureRectangle; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GLContext; /** * A renderer using only GL11 features. * * <p>For correct rendering the OpenGL viewport size must be synchronized.</p> * * @author Matthias Mann * * @see #syncViewportSize() */ public class LWJGLRenderer implements Renderer, LineRenderer { public static final StateKey STATE_LEFT_MOUSE_BUTTON = StateKey.get("leftMouseButton"); public static final StateKey STATE_MIDDLE_MOUSE_BUTTON = StateKey.get("middleMouseButton"); public static final StateKey STATE_RIGHT_MOUSE_BUTTON = StateKey.get("rightMouseButton"); public static final FontParameter.Parameter<Integer> FONTPARAM_OFFSET_X = FontParameter.newParameter("offsetX", 0); public static final FontParameter.Parameter<Integer> FONTPARAM_OFFSET_Y = FontParameter.newParameter("offsetY", 0); public static final FontParameter.Parameter<Integer> FONTPARAM_UNDERLINE_OFFSET = FontParameter.newParameter("underlineOffset", 0); private final IntBuffer ib16; final int maxTextureSize; private int viewportX; private int viewportBottom; private int width; private int height; private boolean hasScissor; private final TintStack tintStateRoot; private final Cursor emptyCursor; private boolean useQuadsForLines; private boolean useSWMouseCursors; private SWCursor swCursor; private int mouseX; private int mouseY; private LWJGLCacheContext cacheContext; private FontMapper fontMapper; final SWCursorAnimState swCursorAnimState; final ArrayList<TextureArea> textureAreas; final ArrayList<TextureAreaRotated> rotatedTextureAreas; final ArrayList<LWJGLDynamicImage> dynamicImages; protected TintStack tintStack; protected final ClipStack clipStack; protected final Rect clipRectTemp; @SuppressWarnings("OverridableMethodCallInConstructor") public LWJGLRenderer() throws LWJGLException { this.ib16 = BufferUtils.createIntBuffer(16); this.textureAreas = new ArrayList<TextureArea>(); this.rotatedTextureAreas = new ArrayList<TextureAreaRotated>(); this.dynamicImages = new ArrayList<LWJGLDynamicImage>(); this.tintStateRoot = new TintStack(); this.tintStack = tintStateRoot; this.clipStack = new ClipStack(); this.clipRectTemp = new Rect(); syncViewportSize(); GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE, ib16); maxTextureSize = ib16.get(0); if(Mouse.isCreated()) { int minCursorSize = Cursor.getMinCursorSize(); IntBuffer tmp = BufferUtils.createIntBuffer(minCursorSize * minCursorSize); emptyCursor = new Cursor(minCursorSize, minCursorSize, minCursorSize/2, minCursorSize/2, 1, tmp, null); } else { emptyCursor = null; } swCursorAnimState = new SWCursorAnimState(); } public boolean isUseQuadsForLines() { return useQuadsForLines; } public void setUseQuadsForLines(boolean useQuadsForLines) { this.useQuadsForLines = useQuadsForLines; } public boolean isUseSWMouseCursors() { return useSWMouseCursors; } /** * Controls if the mouse cursor is rendered via SW or HW cursors. * HW cursors have reduced support for transparency and cursor size. * * This must be set before loading a theme ! * * @param useSWMouseCursors */ public void setUseSWMouseCursors(boolean useSWMouseCursors) { this.useSWMouseCursors = useSWMouseCursors; } public CacheContext createNewCacheContext() { return new LWJGLCacheContext(this); } private LWJGLCacheContext activeCacheContext() { if(cacheContext == null) { setActiveCacheContext(createNewCacheContext()); } return cacheContext; } public CacheContext getActiveCacheContext() { return activeCacheContext(); } public void setActiveCacheContext(CacheContext cc) throws IllegalStateException { if(cc == null) { throw new NullPointerException(); } if(!cc.isValid()) { throw new IllegalStateException("CacheContext is invalid"); } if(!(cc instanceof LWJGLCacheContext)) { throw new IllegalArgumentException("CacheContext object not from this renderer"); } LWJGLCacheContext lwjglCC = (LWJGLCacheContext)cc; if(lwjglCC.renderer != this) { throw new IllegalArgumentException("CacheContext object not from this renderer"); } this.cacheContext = lwjglCC; try { for(TextureArea ta : textureAreas) { ta.destroyRepeatCache(); } for(TextureAreaRotated tar : rotatedTextureAreas) { tar.destroyRepeatCache(); } } finally { textureAreas.clear(); rotatedTextureAreas.clear(); } } /** * <p>Queries the current view port size & position and updates all related * internal state.</p> * * <p>It is important that the internal state matches the OpenGL viewport or * clipping won't work correctly.</p> * * <p>This method should only be called when the viewport size has changed. * It can have negative impact on performance to call every frame.</p> * * @see #getWidth() * @see #getHeight() */ public void syncViewportSize() { ib16.clear(); GL11.glGetInteger(GL11.GL_VIEWPORT, ib16); viewportX = ib16.get(0); width = ib16.get(2); height = ib16.get(3); viewportBottom = ib16.get(1) + height; } /** * Sets the viewport size & position. * <p>This method is preferred over {@link #syncViewportSize() } as it avoids * calling {@link GL11#glGetInteger(int, java.nio.IntBuffer) }.</p> * * @param x the X position (GL_VIEWPORT index 0) * @param y the Y position (GL_VIEWPORT index 1) * @param width the width (GL_VIEWPORT index 2) * @param height the height (GL_VIEWPORT index 3) */ public void setViewport(int x, int y, int width, int height) { this.viewportX = x; this.viewportBottom = y + height; this.width = width; this.height = height; } public long getTimeMillis() { long res = Sys.getTimerResolution(); long time = Sys.getTime(); if(res != 1000) { time = (time * 1000) / res; } return time; } protected void setupGLState() { GL11.glPushAttrib(GL11.GL_ENABLE_BIT|GL11.GL_TRANSFORM_BIT|GL11.GL_HINT_BIT| GL11.GL_COLOR_BUFFER_BIT|GL11.GL_SCISSOR_BIT|GL11.GL_LINE_BIT|GL11.GL_TEXTURE_BIT); GL11.glMatrixMode(GL11.GL_PROJECTION); GL11.glPushMatrix(); GL11.glLoadIdentity(); GL11.glOrtho(0, width, height, 0, -1.0, 1.0); GL11.glMatrixMode(GL11.GL_MODELVIEW); GL11.glPushMatrix(); GL11.glLoadIdentity(); GL11.glEnable(GL11.GL_TEXTURE_2D); GL11.glEnable(GL11.GL_BLEND); GL11.glEnable(GL11.GL_LINE_SMOOTH); GL11.glDisable(GL11.GL_DEPTH_TEST); GL11.glDisable(GL11.GL_LIGHTING); GL11.glDisable(GL11.GL_SCISSOR_TEST); GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST); } protected void revertGLState() { GL11.glPopMatrix(); GL11.glMatrixMode(GL11.GL_PROJECTION); GL11.glPopMatrix(); GL11.glPopAttrib(); } /** * Setup GL to start rendering the GUI. It assumes default GL state. */ public boolean startRendering() { if(width <= 0 || height <= 0) { return false; } prepareForRendering(); setupGLState(); return true; } public void endRendering() { renderSWCursor(); revertGLState(); } /** * Call to revert the GL state to the state before calling * {@link #startRendering()}. * @see #resumeRendering() */ public void pauseRendering() { revertGLState(); } /** * Resume rendering after a call to {@link #pauseRendering()}. */ public void resumeRendering() { hasScissor = false; setupGLState(); setClipRect(); } public int getHeight() { return height; } public int getWidth() { return width; } /** * Retrieves the X position of the OpenGL viewport (index 0 of GL_VIEWPORT) * @return the X position of the OpenGL viewport */ public int getViewportX() { return viewportX; } /** * Retrieves the Y position of the OpenGL viewport (index 1 of GL_VIEWPORT) * @return the Y position of the OpenGL viewport */ public int getViewportY() { return viewportBottom - height; } public Font loadFont(URL url, StateSelect select, FontParameter ... parameterList) throws IOException { if(url == null) { throw new NullPointerException("url"); } if(select == null) { throw new NullPointerException("select"); } if(parameterList == null) { throw new NullPointerException("parameterList"); } if(select.getNumExpressions() + 1 != parameterList.length) { throw new IllegalArgumentException("select.getNumExpressions() + 1 != parameterList.length"); } BitmapFont bmFont = activeCacheContext().loadBitmapFont(url); return new LWJGLFont(this, bmFont, select, parameterList); } public Texture loadTexture(URL url, String formatStr, String filterStr) throws IOException { LWJGLTexture.Format format = LWJGLTexture.Format.COLOR; LWJGLTexture.Filter filter = LWJGLTexture.Filter.LINEAR; if(formatStr != null) { try { format = LWJGLTexture.Format.valueOf(formatStr.toUpperCase(Locale.ENGLISH)); } catch(IllegalArgumentException ex) { getLogger().log(Level.WARNING, "Unknown texture format: {0}", formatStr); } } if(filterStr != null) { try { filter = LWJGLTexture.Filter.valueOf(filterStr.toUpperCase(Locale.ENGLISH)); } catch(IllegalArgumentException ex) { getLogger().log(Level.WARNING, "Unknown texture filter: {0}", filterStr); } } return load(url, format, filter); } public LineRenderer getLineRenderer() { return this; } public OffscreenRenderer getOffscreenRenderer() { return null; } public FontMapper getFontMapper() { return fontMapper; } /** * Installs a font mapper. It is the responsibility of the font mapper to * manage the OpenGL state correctly so that normal rendering by LWJGLRenderer * is not disturbed. * * @param fontMapper the font mapper object - can be null. */ public void setFontMapper(FontMapper fontMapper) { this.fontMapper = fontMapper; } public DynamicImage createDynamicImage(int width, int height) { if(width <= 0) { throw new IllegalArgumentException("width"); } if(height <= 0) { throw new IllegalArgumentException("height"); } if(width > maxTextureSize || height > maxTextureSize) { getLogger().log(Level.WARNING, "requested size {0} x {1} exceeds maximum texture size {3}", new Object[]{ width, height, maxTextureSize }); return null; } int texWidth = width; int texHeight = height; ContextCapabilities caps = GLContext.getCapabilities(); boolean useTextureRectangle = caps.GL_EXT_texture_rectangle || caps.GL_ARB_texture_rectangle; if(!useTextureRectangle && !caps.GL_ARB_texture_non_power_of_two) { texWidth = nextPowerOf2(width); texHeight = nextPowerOf2(height); } // ARB and EXT versions use the same enum ! int proxyTarget = useTextureRectangle ? EXTTextureRectangle.GL_PROXY_TEXTURE_RECTANGLE_EXT : GL11.GL_PROXY_TEXTURE_2D; GL11.glTexImage2D(proxyTarget, 0, GL11.GL_RGBA, texWidth, texHeight, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer)null); ib16.clear(); GL11.glGetTexLevelParameter(proxyTarget, 0, GL11.GL_TEXTURE_WIDTH, ib16); if(ib16.get(0) != texWidth) { getLogger().log(Level.WARNING, "requested size {0} x {1} failed proxy texture test", new Object[]{ texWidth, texHeight }); return null; } // ARB and EXT versions use the same enum ! int target = useTextureRectangle ? EXTTextureRectangle.GL_TEXTURE_RECTANGLE_EXT : GL11.GL_TEXTURE_2D; int id = GL11.glGenTextures(); GL11.glBindTexture(target, id); GL11.glTexImage2D(target, 0, GL11.GL_RGBA, texWidth, texHeight, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer)null); GL11.glTexParameteri(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); GL11.glTexParameteri(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); LWJGLDynamicImage image = new LWJGLDynamicImage(this, target, id, width, height, texWidth, texHeight, Color.WHITE); dynamicImages.add(image); return image; } public Image createGradient(Gradient gradient) { return new GradientImage(this, gradient); } public void clipEnter(int x, int y, int w, int h) { clipStack.push(x, y, w, h); setClipRect(); } public void clipEnter(Rect rect) { clipStack.push(rect); setClipRect(); } public void clipLeave() { clipStack.pop(); setClipRect(); } public boolean clipIsEmpty() { return clipStack.isClipEmpty(); } public void setCursor(MouseCursor cursor) { try { swCursor = null; if(isMouseInsideWindow()) { if(cursor instanceof LWJGLCursor) { setNativeCursor(((LWJGLCursor)cursor).cursor); } else if(cursor instanceof SWCursor) { setNativeCursor(emptyCursor); swCursor = (SWCursor)cursor; } else { setNativeCursor(null); } } } catch(LWJGLException ex) { getLogger().log(Level.WARNING, "Could not set native cursor", ex); } } public void setMousePosition(int mouseX, int mouseY) { this.mouseX = mouseX; this.mouseY = mouseY; } public void setMouseButton(int button, boolean state) { swCursorAnimState.setAnimationState(button, state); } public LWJGLTexture load(URL textureUrl, LWJGLTexture.Format fmt, LWJGLTexture.Filter filter) throws IOException { return load(textureUrl, fmt, filter, null); } public LWJGLTexture load(URL textureUrl, LWJGLTexture.Format fmt, LWJGLTexture.Filter filter, TexturePostProcessing tpp) throws IOException { if(textureUrl == null) { throw new NullPointerException("textureUrl"); } LWJGLCacheContext cc = activeCacheContext(); if(tpp != null) { return cc.createTexture(textureUrl, fmt, filter, tpp); } else { return cc.loadTexture(textureUrl, fmt, filter); } } public void pushGlobalTintColor(float r, float g, float b, float a) { tintStack = tintStack.push(r, g, b, a); } public void popGlobalTintColor() { tintStack = tintStack.pop(); } /** * Pushes a white entry on the tint stack which ignores the previous * tint color. It must be removed by calling {@link #popGlobalTintColor()}. * <p>This is useful when rendering to texture</p> */ public void pushGlobalTintColorReset() { tintStack = tintStack.pushReset(); } /** * Calls GL11.glColor4f() with the specified color multiplied by the current global tint color. * * @param color the color to set */ public void setColor(Color color) { tintStack.setColor(color); } public void drawLine(float[] pts, int numPts, float width, Color color, boolean drawAsLoop) { if(numPts*2 > pts.length) { throw new ArrayIndexOutOfBoundsException(numPts*2); } if(numPts >= 2) { tintStack.setColor(color); GL11.glDisable(GL11.GL_TEXTURE_2D); if(useQuadsForLines) { drawLinesAsQuads(numPts, pts, width, drawAsLoop); } else { drawLinesAsLines(numPts, pts, width, drawAsLoop); } GL11.glEnable(GL11.GL_TEXTURE_2D); } } private void drawLinesAsLines(int numPts, float[] pts, float width, boolean drawAsLoop) { GL11.glLineWidth(width); GL11.glBegin(drawAsLoop ? GL11.GL_LINE_LOOP : GL11.GL_LINE_STRIP); for(int i=0 ; i<numPts ; i++) { GL11.glVertex2f(pts[i*2+0], pts[i*2+1]); } GL11.glEnd(); } private void drawLinesAsQuads(int numPts, float[] pts, float width, boolean drawAsLoop) { width *= 0.5f; GL11.glBegin(GL11.GL_QUADS); for(int i = 1 ; i < numPts ; i++) { drawLineAsQuad(pts[i * 2 - 2], pts[i * 2 - 1], pts[i * 2 + 0], pts[i * 2 + 1], width); } if(drawAsLoop) { int idx = numPts * 2; drawLineAsQuad(pts[idx], pts[idx + 1], pts[0], pts[1], width); } GL11.glEnd(); } private static void drawLineAsQuad(float x0, float y0, float x1, float y1, float w) { float dx = x1 - x0; float dy = y1 - y0; float l = (float)Math.sqrt(dx*dx + dy*dy) / w; dx /= l; dy /= l; GL11.glVertex2f(x0 - dx + dy, y0 - dy - dx); GL11.glVertex2f(x0 - dx - dy, y0 - dy + dx); GL11.glVertex2f(x1 + dx - dy, y1 + dy + dx); GL11.glVertex2f(x1 + dx + dy, y1 + dy - dx); } protected void prepareForRendering() { hasScissor = false; tintStack = tintStateRoot; clipStack.clearStack(); } protected void renderSWCursor() { if(swCursor != null) { tintStack = tintStateRoot; swCursor.render(mouseX, mouseY); } } protected void setNativeCursor(Cursor cursor) throws LWJGLException { Mouse.setNativeCursor(cursor); } protected boolean isMouseInsideWindow() { return Mouse.isInsideWindow(); } protected void getTintedColor(Color color, float[] result) { result[0] = tintStack.r*color.getRed(); result[1] = tintStack.g*color.getGreen(); result[2] = tintStack.b*color.getBlue(); result[3] = tintStack.a*color.getAlpha(); } /** * Computes the tinted color from the given color. * @param color the input color in RGBA order, value range is 0.0 (black) to 255.0 (white). * @param result the tinted color in RGBA order, can be the same array as color. */ protected void getTintedColor(float[] color, float[] result) { result[0] = tintStack.r*color[0]; result[1] = tintStack.g*color[1]; result[2] = tintStack.b*color[2]; result[3] = tintStack.a*color[3]; } protected void setClipRect() { final Rect rect = clipRectTemp; if(clipStack.getClipRect(rect)) { GL11.glScissor(viewportX + rect.getX(), viewportBottom - rect.getBottom(), rect.getWidth(), rect.getHeight()); if(!hasScissor) { GL11.glEnable(GL11.GL_SCISSOR_TEST); hasScissor = true; } } else if(hasScissor) { GL11.glDisable(GL11.GL_SCISSOR_TEST); hasScissor = false; } } /** * Retrieves the active clip region from the top of the stack * @param rect the rect coordinates - may not be updated when clipping is disabled * @return true if clipping is active, false if clipping is disabled */ public boolean getClipRect(Rect rect) { return clipStack.getClipRect(rect); } Logger getLogger() { return Logger.getLogger(LWJGLRenderer.class.getName()); } /** * If the passed value is not a power of 2 then return the next highest power of 2 * otherwise the value is returned unchanged. * * <p> Warren Jr., Henry S. (2002). Hacker's Delight. Addison Wesley. pp. 48. ISBN 978-0201914658</p> * * @param i a non negative number <= 2^31 * @return the smallest power of 2 which is >= i */ private static int nextPowerOf2(int i) { i--; i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); return i+1; } private static class SWCursorAnimState implements AnimationState { private final long[] lastTime; private final boolean[] active; SWCursorAnimState() { lastTime = new long[3]; active = new boolean[3]; } void setAnimationState(int idx, boolean isActive) { if(idx >= 0 && idx < 3 && active[idx] != isActive) { lastTime[idx] = Sys.getTime(); active[idx] = isActive; } } public int getAnimationTime(StateKey state) { long curTime = Sys.getTime(); int idx = getMouseButton(state); if(idx >= 0) { curTime -= lastTime[idx]; } return (int)curTime & Integer.MAX_VALUE; } public boolean getAnimationState(StateKey state) { int idx = getMouseButton(state); if(idx >= 0) { return active[idx]; } return false; } public boolean getShouldAnimateState(StateKey state) { return true; } private int getMouseButton(StateKey key) { if(key == STATE_LEFT_MOUSE_BUTTON) { return Event.MOUSE_LBUTTON; } if(key == STATE_MIDDLE_MOUSE_BUTTON) { return Event.MOUSE_MBUTTON; } if(key == STATE_RIGHT_MOUSE_BUTTON) { return Event.MOUSE_RBUTTON; } return -1; } } }