2 * Copyright (c) 2008-2011, Matthias Mann
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
9 * * Redistributions of source code must retain the above copyright notice,
10 * this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * * Neither the name of Matthias Mann nor the names of its contributors may
15 * be used to endorse or promote products derived from this software
16 * without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
22 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 package de.matthiasmann.twl.renderer.lwjgl;
32 import de.matthiasmann.twl.Color;
33 import de.matthiasmann.twl.Event;
34 import de.matthiasmann.twl.Rect;
35 import de.matthiasmann.twl.renderer.AnimationState;
36 import de.matthiasmann.twl.renderer.AnimationState.StateKey;
37 import de.matthiasmann.twl.renderer.CacheContext;
38 import de.matthiasmann.twl.renderer.DynamicImage;
39 import de.matthiasmann.twl.renderer.FontParameter;
40 import de.matthiasmann.twl.renderer.Gradient;
41 import de.matthiasmann.twl.renderer.Image;
42 import de.matthiasmann.twl.renderer.MouseCursor;
43 import de.matthiasmann.twl.renderer.Font;
44 import de.matthiasmann.twl.renderer.FontMapper;
45 import de.matthiasmann.twl.renderer.LineRenderer;
46 import de.matthiasmann.twl.renderer.OffscreenRenderer;
47 import de.matthiasmann.twl.renderer.Renderer;
48 import de.matthiasmann.twl.renderer.Texture;
49 import de.matthiasmann.twl.utils.ClipStack;
50 import de.matthiasmann.twl.utils.StateSelect;
51 import java.io.IOException;
53 import java.nio.ByteBuffer;
54 import java.nio.IntBuffer;
55 import java.util.ArrayList;
56 import java.util.Locale;
57 import java.util.logging.Level;
58 import java.util.logging.Logger;
59 import org.lwjgl.BufferUtils;
60 import org.lwjgl.LWJGLException;
62 import org.lwjgl.input.Cursor;
63 import org.lwjgl.input.Mouse;
64 import org.lwjgl.opengl.ContextCapabilities;
65 import org.lwjgl.opengl.EXTTextureRectangle;
66 import org.lwjgl.opengl.GL11;
67 import org.lwjgl.opengl.GLContext;
70 * A renderer using only GL11 features.
72 * <p>For correct rendering the OpenGL viewport size must be synchronized.</p>
74 * @author Matthias Mann
76 * @see #syncViewportSize()
78 public class LWJGLRenderer implements Renderer, LineRenderer {
80 public static final StateKey STATE_LEFT_MOUSE_BUTTON = StateKey.get("leftMouseButton");
81 public static final StateKey STATE_MIDDLE_MOUSE_BUTTON = StateKey.get("middleMouseButton");
82 public static final StateKey STATE_RIGHT_MOUSE_BUTTON = StateKey.get("rightMouseButton");
84 public static final FontParameter.Parameter<Integer> FONTPARAM_OFFSET_X = FontParameter.newParameter("offsetX", 0);
85 public static final FontParameter.Parameter<Integer> FONTPARAM_OFFSET_Y = FontParameter.newParameter("offsetY", 0);
86 public static final FontParameter.Parameter<Integer> FONTPARAM_UNDERLINE_OFFSET = FontParameter.newParameter("underlineOffset", 0);
88 private final IntBuffer ib16;
89 final int maxTextureSize;
91 private int viewportX;
92 private int viewportBottom;
95 private boolean hasScissor;
96 private final TintStack tintStateRoot;
97 private final Cursor emptyCursor;
98 private boolean useQuadsForLines;
99 private boolean useSWMouseCursors;
100 private SWCursor swCursor;
103 private LWJGLCacheContext cacheContext;
104 private FontMapper fontMapper;
106 final SWCursorAnimState swCursorAnimState;
107 final ArrayList<TextureArea> textureAreas;
108 final ArrayList<TextureAreaRotated> rotatedTextureAreas;
109 final ArrayList<LWJGLDynamicImage> dynamicImages;
111 protected TintStack tintStack;
112 protected final ClipStack clipStack;
113 protected final Rect clipRectTemp;
115 @SuppressWarnings("OverridableMethodCallInConstructor")
116 public LWJGLRenderer() throws LWJGLException {
117 this.ib16 = BufferUtils.createIntBuffer(16);
118 this.textureAreas = new ArrayList<TextureArea>();
119 this.rotatedTextureAreas = new ArrayList<TextureAreaRotated>();
120 this.dynamicImages = new ArrayList<LWJGLDynamicImage>();
121 this.tintStateRoot = new TintStack();
122 this.tintStack = tintStateRoot;
123 this.clipStack = new ClipStack();
124 this.clipRectTemp = new Rect();
127 GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE, ib16);
128 maxTextureSize = ib16.get(0);
130 if(Mouse.isCreated()) {
131 int minCursorSize = Cursor.getMinCursorSize();
132 IntBuffer tmp = BufferUtils.createIntBuffer(minCursorSize * minCursorSize);
133 emptyCursor = new Cursor(minCursorSize, minCursorSize,
134 minCursorSize/2, minCursorSize/2, 1, tmp, null);
139 swCursorAnimState = new SWCursorAnimState();
142 public boolean isUseQuadsForLines() {
143 return useQuadsForLines;
146 public void setUseQuadsForLines(boolean useQuadsForLines) {
147 this.useQuadsForLines = useQuadsForLines;
150 public boolean isUseSWMouseCursors() {
151 return useSWMouseCursors;
155 * Controls if the mouse cursor is rendered via SW or HW cursors.
156 * HW cursors have reduced support for transparency and cursor size.
158 * This must be set before loading a theme !
160 * @param useSWMouseCursors
162 public void setUseSWMouseCursors(boolean useSWMouseCursors) {
163 this.useSWMouseCursors = useSWMouseCursors;
166 public CacheContext createNewCacheContext() {
167 return new LWJGLCacheContext(this);
170 private LWJGLCacheContext activeCacheContext() {
171 if(cacheContext == null) {
172 setActiveCacheContext(createNewCacheContext());
177 public CacheContext getActiveCacheContext() {
178 return activeCacheContext();
181 public void setActiveCacheContext(CacheContext cc) throws IllegalStateException {
183 throw new NullPointerException();
186 throw new IllegalStateException("CacheContext is invalid");
188 if(!(cc instanceof LWJGLCacheContext)) {
189 throw new IllegalArgumentException("CacheContext object not from this renderer");
191 LWJGLCacheContext lwjglCC = (LWJGLCacheContext)cc;
192 if(lwjglCC.renderer != this) {
193 throw new IllegalArgumentException("CacheContext object not from this renderer");
195 this.cacheContext = lwjglCC;
197 for(TextureArea ta : textureAreas) {
198 ta.destroyRepeatCache();
200 for(TextureAreaRotated tar : rotatedTextureAreas) {
201 tar.destroyRepeatCache();
204 textureAreas.clear();
205 rotatedTextureAreas.clear();
210 * <p>Queries the current view port size & position and updates all related
211 * internal state.</p>
213 * <p>It is important that the internal state matches the OpenGL viewport or
214 * clipping won't work correctly.</p>
216 * <p>This method should only be called when the viewport size has changed.
217 * It can have negative impact on performance to call every frame.</p>
222 public void syncViewportSize() {
224 GL11.glGetInteger(GL11.GL_VIEWPORT, ib16);
225 viewportX = ib16.get(0);
227 height = ib16.get(3);
228 viewportBottom = ib16.get(1) + height;
232 * Sets the viewport size & position.
233 * <p>This method is preferred over {@link #syncViewportSize() } as it avoids
234 * calling {@link GL11#glGetInteger(int, java.nio.IntBuffer) }.</p>
236 * @param x the X position (GL_VIEWPORT index 0)
237 * @param y the Y position (GL_VIEWPORT index 1)
238 * @param width the width (GL_VIEWPORT index 2)
239 * @param height the height (GL_VIEWPORT index 3)
241 public void setViewport(int x, int y, int width, int height) {
243 this.viewportBottom = y + height;
245 this.height = height;
248 public long getTimeMillis() {
249 long res = Sys.getTimerResolution();
250 long time = Sys.getTime();
252 time = (time * 1000) / res;
257 protected void setupGLState() {
258 GL11.glPushAttrib(GL11.GL_ENABLE_BIT|GL11.GL_TRANSFORM_BIT|GL11.GL_HINT_BIT|
259 GL11.GL_COLOR_BUFFER_BIT|GL11.GL_SCISSOR_BIT|GL11.GL_LINE_BIT|GL11.GL_TEXTURE_BIT);
260 GL11.glMatrixMode(GL11.GL_PROJECTION);
262 GL11.glLoadIdentity();
263 GL11.glOrtho(0, width, height, 0, -1.0, 1.0);
264 GL11.glMatrixMode(GL11.GL_MODELVIEW);
266 GL11.glLoadIdentity();
267 GL11.glEnable(GL11.GL_TEXTURE_2D);
268 GL11.glEnable(GL11.GL_BLEND);
269 GL11.glEnable(GL11.GL_LINE_SMOOTH);
270 GL11.glDisable(GL11.GL_DEPTH_TEST);
271 GL11.glDisable(GL11.GL_LIGHTING);
272 GL11.glDisable(GL11.GL_SCISSOR_TEST);
273 GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
274 GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST);
277 protected void revertGLState() {
279 GL11.glMatrixMode(GL11.GL_PROJECTION);
285 * Setup GL to start rendering the GUI. It assumes default GL state.
287 public boolean startRendering() {
288 if(width <= 0 || height <= 0) {
292 prepareForRendering();
298 public void endRendering() {
304 * Call to revert the GL state to the state before calling
305 * {@link #startRendering()}.
306 * @see #resumeRendering()
308 public void pauseRendering() {
313 * Resume rendering after a call to {@link #pauseRendering()}.
315 public void resumeRendering() {
321 public int getHeight() {
325 public int getWidth() {
330 * Retrieves the X position of the OpenGL viewport (index 0 of GL_VIEWPORT)
331 * @return the X position of the OpenGL viewport
333 public int getViewportX() {
338 * Retrieves the Y position of the OpenGL viewport (index 1 of GL_VIEWPORT)
339 * @return the Y position of the OpenGL viewport
341 public int getViewportY() {
342 return viewportBottom - height;
345 public Font loadFont(URL url, StateSelect select, FontParameter ... parameterList) throws IOException {
347 throw new NullPointerException("url");
350 throw new NullPointerException("select");
352 if(parameterList == null) {
353 throw new NullPointerException("parameterList");
355 if(select.getNumExpressions() + 1 != parameterList.length) {
356 throw new IllegalArgumentException("select.getNumExpressions() + 1 != parameterList.length");
358 BitmapFont bmFont = activeCacheContext().loadBitmapFont(url);
359 return new LWJGLFont(this, bmFont, select, parameterList);
362 public Texture loadTexture(URL url, String formatStr, String filterStr) throws IOException {
363 LWJGLTexture.Format format = LWJGLTexture.Format.COLOR;
364 LWJGLTexture.Filter filter = LWJGLTexture.Filter.LINEAR;
365 if(formatStr != null) {
367 format = LWJGLTexture.Format.valueOf(formatStr.toUpperCase(Locale.ENGLISH));
368 } catch(IllegalArgumentException ex) {
369 getLogger().log(Level.WARNING, "Unknown texture format: {0}", formatStr);
372 if(filterStr != null) {
374 filter = LWJGLTexture.Filter.valueOf(filterStr.toUpperCase(Locale.ENGLISH));
375 } catch(IllegalArgumentException ex) {
376 getLogger().log(Level.WARNING, "Unknown texture filter: {0}", filterStr);
379 return load(url, format, filter);
382 public LineRenderer getLineRenderer() {
386 public OffscreenRenderer getOffscreenRenderer() {
390 public FontMapper getFontMapper() {
395 * Installs a font mapper. It is the responsibility of the font mapper to
396 * manage the OpenGL state correctly so that normal rendering by LWJGLRenderer
399 * @param fontMapper the font mapper object - can be null.
401 public void setFontMapper(FontMapper fontMapper) {
402 this.fontMapper = fontMapper;
405 public DynamicImage createDynamicImage(int width, int height) {
407 throw new IllegalArgumentException("width");
410 throw new IllegalArgumentException("height");
412 if(width > maxTextureSize || height > maxTextureSize) {
413 getLogger().log(Level.WARNING, "requested size {0} x {1} exceeds maximum texture size {3}",
414 new Object[]{ width, height, maxTextureSize });
418 int texWidth = width;
419 int texHeight = height;
421 ContextCapabilities caps = GLContext.getCapabilities();
422 boolean useTextureRectangle = caps.GL_EXT_texture_rectangle || caps.GL_ARB_texture_rectangle;
424 if(!useTextureRectangle && !caps.GL_ARB_texture_non_power_of_two) {
425 texWidth = nextPowerOf2(width);
426 texHeight = nextPowerOf2(height);
429 // ARB and EXT versions use the same enum !
430 int proxyTarget = useTextureRectangle ?
431 EXTTextureRectangle.GL_PROXY_TEXTURE_RECTANGLE_EXT : GL11.GL_PROXY_TEXTURE_2D;
433 GL11.glTexImage2D(proxyTarget, 0, GL11.GL_RGBA, texWidth, texHeight, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer)null);
435 GL11.glGetTexLevelParameter(proxyTarget, 0, GL11.GL_TEXTURE_WIDTH, ib16);
436 if(ib16.get(0) != texWidth) {
437 getLogger().log(Level.WARNING, "requested size {0} x {1} failed proxy texture test",
438 new Object[]{ texWidth, texHeight });
442 // ARB and EXT versions use the same enum !
443 int target = useTextureRectangle ?
444 EXTTextureRectangle.GL_TEXTURE_RECTANGLE_EXT : GL11.GL_TEXTURE_2D;
445 int id = GL11.glGenTextures();
447 GL11.glBindTexture(target, id);
448 GL11.glTexImage2D(target, 0, GL11.GL_RGBA, texWidth, texHeight, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer)null);
449 GL11.glTexParameteri(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
450 GL11.glTexParameteri(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
452 LWJGLDynamicImage image = new LWJGLDynamicImage(this, target, id, width, height, texWidth, texHeight, Color.WHITE);
453 dynamicImages.add(image);
457 public Image createGradient(Gradient gradient) {
458 return new GradientImage(this, gradient);
461 public void clipEnter(int x, int y, int w, int h) {
462 clipStack.push(x, y, w, h);
466 public void clipEnter(Rect rect) {
467 clipStack.push(rect);
471 public void clipLeave() {
476 public boolean clipIsEmpty() {
477 return clipStack.isClipEmpty();
480 public void setCursor(MouseCursor cursor) {
483 if(isMouseInsideWindow()) {
484 if(cursor instanceof LWJGLCursor) {
485 setNativeCursor(((LWJGLCursor)cursor).cursor);
486 } else if(cursor instanceof SWCursor) {
487 setNativeCursor(emptyCursor);
488 swCursor = (SWCursor)cursor;
490 setNativeCursor(null);
493 } catch(LWJGLException ex) {
494 getLogger().log(Level.WARNING, "Could not set native cursor", ex);
498 public void setMousePosition(int mouseX, int mouseY) {
499 this.mouseX = mouseX;
500 this.mouseY = mouseY;
503 public void setMouseButton(int button, boolean state) {
504 swCursorAnimState.setAnimationState(button, state);
507 public LWJGLTexture load(URL textureUrl, LWJGLTexture.Format fmt, LWJGLTexture.Filter filter) throws IOException {
508 return load(textureUrl, fmt, filter, null);
511 public LWJGLTexture load(URL textureUrl, LWJGLTexture.Format fmt, LWJGLTexture.Filter filter, TexturePostProcessing tpp) throws IOException {
512 if(textureUrl == null) {
513 throw new NullPointerException("textureUrl");
515 LWJGLCacheContext cc = activeCacheContext();
517 return cc.createTexture(textureUrl, fmt, filter, tpp);
519 return cc.loadTexture(textureUrl, fmt, filter);
523 public void pushGlobalTintColor(float r, float g, float b, float a) {
524 tintStack = tintStack.push(r, g, b, a);
527 public void popGlobalTintColor() {
528 tintStack = tintStack.pop();
532 * Pushes a white entry on the tint stack which ignores the previous
533 * tint color. It must be removed by calling {@link #popGlobalTintColor()}.
534 * <p>This is useful when rendering to texture</p>
536 public void pushGlobalTintColorReset() {
537 tintStack = tintStack.pushReset();
541 * Calls GL11.glColor4f() with the specified color multiplied by the current global tint color.
543 * @param color the color to set
545 public void setColor(Color color) {
546 tintStack.setColor(color);
549 public void drawLine(float[] pts, int numPts, float width, Color color, boolean drawAsLoop) {
550 if(numPts*2 > pts.length) {
551 throw new ArrayIndexOutOfBoundsException(numPts*2);
554 tintStack.setColor(color);
555 GL11.glDisable(GL11.GL_TEXTURE_2D);
556 if(useQuadsForLines) {
557 drawLinesAsQuads(numPts, pts, width, drawAsLoop);
559 drawLinesAsLines(numPts, pts, width, drawAsLoop);
561 GL11.glEnable(GL11.GL_TEXTURE_2D);
565 private void drawLinesAsLines(int numPts, float[] pts, float width, boolean drawAsLoop) {
566 GL11.glLineWidth(width);
567 GL11.glBegin(drawAsLoop ? GL11.GL_LINE_LOOP : GL11.GL_LINE_STRIP);
568 for(int i=0 ; i<numPts ; i++) {
569 GL11.glVertex2f(pts[i*2+0], pts[i*2+1]);
574 private void drawLinesAsQuads(int numPts, float[] pts, float width, boolean drawAsLoop) {
576 GL11.glBegin(GL11.GL_QUADS);
577 for(int i = 1 ; i < numPts ; i++) {
578 drawLineAsQuad(pts[i * 2 - 2], pts[i * 2 - 1], pts[i * 2 + 0], pts[i * 2 + 1], width);
581 int idx = numPts * 2;
582 drawLineAsQuad(pts[idx], pts[idx + 1], pts[0], pts[1], width);
587 private static void drawLineAsQuad(float x0, float y0, float x1, float y1, float w) {
590 float l = (float)Math.sqrt(dx*dx + dy*dy) / w;
593 GL11.glVertex2f(x0 - dx + dy, y0 - dy - dx);
594 GL11.glVertex2f(x0 - dx - dy, y0 - dy + dx);
595 GL11.glVertex2f(x1 + dx - dy, y1 + dy + dx);
596 GL11.glVertex2f(x1 + dx + dy, y1 + dy - dx);
599 protected void prepareForRendering() {
601 tintStack = tintStateRoot;
602 clipStack.clearStack();
605 protected void renderSWCursor() {
606 if(swCursor != null) {
607 tintStack = tintStateRoot;
608 swCursor.render(mouseX, mouseY);
612 protected void setNativeCursor(Cursor cursor) throws LWJGLException {
613 Mouse.setNativeCursor(cursor);
616 protected boolean isMouseInsideWindow() {
617 return Mouse.isInsideWindow();
620 protected void getTintedColor(Color color, float[] result) {
621 result[0] = tintStack.r*color.getRed();
622 result[1] = tintStack.g*color.getGreen();
623 result[2] = tintStack.b*color.getBlue();
624 result[3] = tintStack.a*color.getAlpha();
628 * Computes the tinted color from the given color.
629 * @param color the input color in RGBA order, value range is 0.0 (black) to 255.0 (white).
630 * @param result the tinted color in RGBA order, can be the same array as color.
632 protected void getTintedColor(float[] color, float[] result) {
633 result[0] = tintStack.r*color[0];
634 result[1] = tintStack.g*color[1];
635 result[2] = tintStack.b*color[2];
636 result[3] = tintStack.a*color[3];
639 protected void setClipRect() {
640 final Rect rect = clipRectTemp;
641 if(clipStack.getClipRect(rect)) {
642 GL11.glScissor(viewportX + rect.getX(), viewportBottom - rect.getBottom(), rect.getWidth(), rect.getHeight());
644 GL11.glEnable(GL11.GL_SCISSOR_TEST);
647 } else if(hasScissor) {
648 GL11.glDisable(GL11.GL_SCISSOR_TEST);
654 return Logger.getLogger(LWJGLRenderer.class.getName());
658 * If the passed value is not a power of 2 then return the next highest power of 2
659 * otherwise the value is returned unchanged.
661 * <p> Warren Jr., Henry S. (2002). Hacker's Delight. Addison Wesley. pp. 48. ISBN 978-0201914658</p>
663 * @param i a non negative number <= 2^31
664 * @return the smallest power of 2 which is >= i
666 private static int nextPowerOf2(int i) {
676 private static class SWCursorAnimState implements AnimationState {
677 private final long[] lastTime;
678 private final boolean[] active;
680 SWCursorAnimState() {
681 lastTime = new long[3];
682 active = new boolean[3];
685 void setAnimationState(int idx, boolean isActive) {
686 if(idx >= 0 && idx < 3 && active[idx] != isActive) {
687 lastTime[idx] = Sys.getTime();
688 active[idx] = isActive;
692 public int getAnimationTime(StateKey state) {
693 long curTime = Sys.getTime();
694 int idx = getMouseButton(state);
696 curTime -= lastTime[idx];
698 return (int)curTime & Integer.MAX_VALUE;
701 public boolean getAnimationState(StateKey state) {
702 int idx = getMouseButton(state);
709 public boolean getShouldAnimateState(StateKey state) {
713 private int getMouseButton(StateKey key) {
714 if(key == STATE_LEFT_MOUSE_BUTTON) {
715 return Event.MOUSE_LBUTTON;
717 if(key == STATE_MIDDLE_MOUSE_BUTTON) {
718 return Event.MOUSE_MBUTTON;
720 if(key == STATE_RIGHT_MOUSE_BUTTON) {
721 return Event.MOUSE_RBUTTON;