From 41c7825728312920f4f751c8d85b3c0e9dce6d4d Mon Sep 17 00:00:00 2001 From: Peter Tissen Date: Mon, 30 Mar 2015 14:19:39 +0200 Subject: [PATCH] New optional slider style New slider rendering works by rendering the slider to an offscreen buffer Add CurveRenderState.java and FrameBufferCache.java that were forgotten in the last commit --- res/slidergradient.png | Bin 0 -> 3918 bytes src/itdelatrisu/opsu/GameData.java | 19 +- src/itdelatrisu/opsu/GameImage.java | 1 + src/itdelatrisu/opsu/Options.java | 29 ++ src/itdelatrisu/opsu/objects/Slider.java | 48 +- .../opsu/objects/curves/Curve.java | 53 ++- .../opsu/render/CurveRenderState.java | 428 ++++++++++++++++++ .../opsu/render/FrameBufferCache.java | 123 +++++ src/itdelatrisu/opsu/render/Rendertarget.java | 110 +++++ src/itdelatrisu/opsu/states/Game.java | 4 + src/itdelatrisu/opsu/states/OptionsMenu.java | 3 +- 11 files changed, 790 insertions(+), 28 deletions(-) create mode 100644 res/slidergradient.png create mode 100644 src/itdelatrisu/opsu/render/CurveRenderState.java create mode 100644 src/itdelatrisu/opsu/render/FrameBufferCache.java create mode 100644 src/itdelatrisu/opsu/render/Rendertarget.java diff --git a/res/slidergradient.png b/res/slidergradient.png new file mode 100644 index 0000000000000000000000000000000000000000..6939923f2c7e555ce48a51772c4eea3fa35b8d80 GIT binary patch literal 3918 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 iter =hitResultList.iterator(); + while(iter.hasNext()) + { + OsuHitObjectResult hitObj = iter.next(); + if(hitObj != null && hitObj.curve != null) + { + hitObj.curve.discardCache(); + } + } + } hitResultList = new LinkedBlockingDeque(); hitErrorList = new LinkedBlockingDeque(); fullObjectCount = 0; @@ -939,8 +951,13 @@ public class GameData { } hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME); - } else + } else{ + if (hitResult.curve !=null) + { + hitResult.curve.discardCache(); + } iter.remove(); + } } } diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 9dbeb55c..411188d0 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -110,6 +110,7 @@ public enum GameImage { APPROACHCIRCLE ("approachcircle", "png"), // Slider + SLIDER_GRADIENT ("slidergradient", "png"), SLIDER_BALL ("sliderb", "sliderb%d", "png"), SLIDER_FOLLOWCIRCLE ("sliderfollowcircle", "png"), REVERSEARROW ("reversearrow", "png"), diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 292aa951..0c7e42b1 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -228,6 +228,7 @@ public class Options { UI.getCursor().reset(); } }, + NEW_SLIDER("Enable New Slider", "Use the new Slider style (requires OpenGL 3.0).",false), DYNAMIC_BACKGROUND ("Enable Dynamic Backgrounds", "The song background will be used as the main menu background.", true), BACKGROUND_DIM ("Background Dim", "Percentage to dim the background image during gameplay.", 50, 0, 100), FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "Override the song background with the default playfield background.", false), @@ -484,6 +485,9 @@ public class Options { /** The current skin. */ private static Skin skin; + /** Resolution that the latest Container passed to "setDisplayMode" was set to. */ + private static Resolution lastSetResolution = Resolution.RES_1024_768; + /** Frame limiters. */ private static final int[] targetFPS = { 60, 120, 240 }; @@ -593,6 +597,8 @@ public class Options { // set borderless window if dimensions match screen size boolean borderless = (screenWidth == resolution.getWidth() && screenHeight == resolution.getHeight()); System.setProperty("org.lwjgl.opengl.Window.undecorated", Boolean.toString(borderless)); + + lastSetResolution = resolution; } // /** @@ -887,6 +893,24 @@ public class Options { return replayDir; } + /** + * Returns the horizontal Resolution. + * If no game has been started then a default value is returned. + * @return the horizontal resolution of the latest game instance to be started. + */ + public static int getLatestResolutionWidth() { + return lastSetResolution.getWidth(); + } + + /** + * Returns the vertical Resolution. + * If no game has been started then a default value is returned. + * @return the vertical resolution of the latest game instance to be started. + */ + public static int getLatestResolutionHeight() { + return lastSetResolution.getHeight(); + } + /** * Returns the current skin directory. * If invalid, this will create a "Skins" folder in the root directory. @@ -1071,6 +1095,9 @@ public class Options { case "NewCursor": GameOption.NEW_CURSOR.setValue(Boolean.parseBoolean(value)); break; + case "NewSlider": + GameOption.NEW_SLIDER.setValue(Boolean.parseBoolean(value)); + break; case "DynamicBackground": GameOption.DYNAMIC_BACKGROUND.setValue(Boolean.parseBoolean(value)); break; @@ -1222,6 +1249,8 @@ public class Options { writer.newLine(); writer.write(String.format("NewCursor = %b", isNewCursorEnabled())); writer.newLine(); + writer.write(String.format("NewSlider = %b", GameOption.NEW_SLIDER.getBooleanValue())); + writer.newLine(); writer.write(String.format("DynamicBackground = %b", isDynamicBackgroundEnabled())); writer.newLine(); writer.write(String.format("LoadVerbose = %b", isLoadVerbose())); diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 00975303..bcf3f0ab 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -30,12 +30,20 @@ import itdelatrisu.opsu.objects.curves.CatmullCurve; import itdelatrisu.opsu.objects.curves.CircumscribedCircle; import itdelatrisu.opsu.objects.curves.Curve; import itdelatrisu.opsu.objects.curves.LinearBezier; +import itdelatrisu.opsu.render.Rendertarget; import itdelatrisu.opsu.states.Game; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL14; +import org.lwjgl.opengl.GL30; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; import org.newdawn.slick.Graphics; import org.newdawn.slick.Image; +import org.newdawn.slick.util.Log; /** * Data type representing a slider object. @@ -50,6 +58,9 @@ public class Slider implements GameObject { /** Rate at which slider ticks are placed. */ private static float sliderTickRate = 1.0f; + /** Scaling factor for display elements */ + private static int diameter = 1; + /** The amount of time, in milliseconds, to fade in the slider. */ private static final int FADE_IN_TIME = 375; @@ -111,7 +122,7 @@ public class Slider implements GameObject { containerWidth = container.getWidth(); containerHeight = container.getHeight(); - int diameter = (int) (104 - (circleSize * 8)); + diameter = (int) (104 - (circleSize * 8)); diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) // slider ball @@ -170,12 +181,26 @@ public class Slider implements GameObject { float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME; float approachScale = 1 + scale * 3; float alpha = Utils.clamp(1 - fadeinScale, 0, 1); + boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber(); float oldAlpha = Utils.COLOR_WHITE_FADE.a; Utils.COLOR_WHITE_FADE.a = color.a = alpha; + Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); + Image hitCircle = GameImage.HITCIRCLE.getImage(); + float[] endPos = curve.pointAt(1); - // curve curve.draw(color); + color.a = alpha; + + // end circle + hitCircle.drawCentered(endPos[0], endPos[1], color); + hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE); + + // start circle + hitCircle.drawCentered(x, y, color); + if (!overlayAboveNumber) { + hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE); + } // ticks if (ticksT != null) { @@ -185,25 +210,11 @@ public class Slider implements GameObject { tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE); } } - - Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); - Image hitCircle = GameImage.HITCIRCLE.getImage(); - - // end circle - float[] endPos = curve.pointAt(1); - hitCircle.drawCentered(endPos[0], endPos[1], color); - hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE); - - // start circle - hitCircle.drawCentered(x, y, color); - boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber(); - if (!overlayAboveNumber) - hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE); if (sliderClickedInitial) ; // don't draw current combo number if already clicked else data.drawSymbolNumber(hitObject.getComboNumber(), x, y, - hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha); + hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha); if (overlayAboveNumber) hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE); @@ -388,6 +399,7 @@ public class Slider implements GameObject { // calculate and send slider result hitResult(); + return true; } @@ -469,6 +481,8 @@ public class Slider implements GameObject { this.curve = new CatmullCurve(hitObject, color); else this.curve = new LinearBezier(hitObject, color, hitObject.getSliderType() == HitObject.SLIDER_LINEAR); + + this.curve.setScale(diameter );//* 118 / 128); } @Override diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index 77a25829..0b96abc6 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -18,12 +18,15 @@ package itdelatrisu.opsu.objects.curves; +import itdelatrisu.opsu.render.CurveRenderState; import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.beatmap.HitObject; +import itdelatrisu.opsu.Options; import org.newdawn.slick.Color; import org.newdawn.slick.Image; +import org.newdawn.slick.util.Log; /** * Representation of a curve. @@ -42,6 +45,12 @@ public abstract class Curve { /** The scaled slider x, y coordinate lists. */ protected float[] sliderX, sliderY; + + /** scaling factor for drawing. */ + protected static float scale; + + /** Per-curve render-state used for the new style curve renders*/ + private CurveRenderState renderState; /** Points along the curve (set by inherited classes). */ protected Vec2f[] curve; @@ -57,6 +66,8 @@ public abstract class Curve { this.y = hitObject.getScaledY(); this.sliderX = hitObject.getScaledSliderX(); this.sliderY = hitObject.getScaledSliderY(); + this.scale = 100; + this.renderState = null; } /** @@ -71,15 +82,26 @@ public abstract class Curve { * @param color the color filter */ public void draw(Color color) { - if (curve == null) - return; - - Image hitCircle = GameImage.HITCIRCLE.getImage(); - Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); - for (int i = 0; i < curve.length; i++) - hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE); - for (int i = 0; i < curve.length; i++) - hitCircle.drawCentered(curve[i].x, curve[i].y, color); + if ( curve == null){ + Log.error("draw curve"+this); + return; + } + if (Options.GameOption.NEW_SLIDER.getBooleanValue()) { + if(renderState == null) + { + renderState = new CurveRenderState(scale,hitObject); + } + renderState.draw(color,curve); + } else { + Image hitCircle = GameImage.HITCIRCLE.getImage(); + Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); + for (int i = 0; i < curve.length; i++) { + hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE); + } + for (int i = 0; i < curve.length; i++){ + hitCircle.drawCentered(curve[i].x, curve[i].y, color); + } + } } /** @@ -104,10 +126,23 @@ public abstract class Curve { */ public float getY(int i) { return (i == 0) ? y : sliderY[i - 1]; } + /** + * Set the scaling factor. + * @param factor the new scaling factor for the UI representation + */ + public static void setScale(float factor) { + scale = factor; + } + /** * Linear interpolation of a and b at t. */ protected float lerp(float a, float b, float t) { return a * (1 - t) + b * t; } + + public void discardCache() { + if(renderState != null) + renderState.discardCache(); + } } diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java new file mode 100644 index 00000000..9afda458 --- /dev/null +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -0,0 +1,428 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ +package itdelatrisu.opsu.render; + +import itdelatrisu.opsu.GameImage; +import itdelatrisu.opsu.Options; +import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.beatmap.HitObject; +import itdelatrisu.opsu.objects.curves.Vec2f; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL14; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL30; +import org.newdawn.slick.Color; +import org.newdawn.slick.Image; +import org.newdawn.slick.util.Log; + +/** + * Hold the temporary render state that needs to be restored again after the new + * style curves are drawn. + */ +public class CurveRenderState { + + /** cached drawn slider, only used if new style sliders are activated */ + public Rendertarget fbo; + + /** thickness of the curve */ + float scale; + + /** The HitObject associated with the curve to be drawn */ + HitObject hitObject; + + /** Static state that's needed to draw the new style curves */ + private static final NewCurveStyleState staticState = new NewCurveStyleState(); + + public CurveRenderState(float scale, HitObject hitObject) { + fbo = null; + this.scale = scale; + this.hitObject = hitObject; + } + + public void draw(Color color, Vec2f[] curve) { + float alpha = color.a; + //if this curve hasn't been drawn, draw it and cache the result + if (fbo == null) { + FrameBufferCache cache = FrameBufferCache.getInstance(); + Rendertarget mapping = cache.get(hitObject); + if (mapping == null) { + mapping = cache.insert(hitObject); + } + fbo = mapping; + + int old_fb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING); + int old_tex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + + GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID()); + GL11.glViewport(0, 0, fbo.width, fbo.height); + GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + Utils.COLOR_WHITE_FADE.a = 1.0f; + this.draw_curve(color, curve); + color.a = 1f; + + GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_tex); + GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_fb); + Utils.COLOR_WHITE_FADE.a = alpha; + } + + //draw a fullscreen quad with the texture that contains the curve + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_TEXTURE_1D); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.getTextureID()); + GL11.glBegin(GL11.GL_QUADS); + GL11.glColor4f(1.0f, 1.0f, 1.0f, alpha); + GL11.glTexCoord2f(1.0f, 1.0f); + GL11.glVertex2i(fbo.width, 0); + GL11.glTexCoord2f(0.0f, 1.0f); + GL11.glVertex2i(0, 0); + GL11.glTexCoord2f(0.0f, 0.0f); + GL11.glVertex2i(0, fbo.height); + GL11.glTexCoord2f(1.0f, 0.0f); + GL11.glVertex2i(fbo.width, fbo.height); + GL11.glEnd(); + } + + public void discardCache() { + fbo = null; + FrameBufferCache.getInstance().freeMappingFor(hitObject); + } + + private class RenderState { + boolean smoothedPoly; + boolean blendEnabled; + boolean depthEnabled; + boolean depthWriteEnabled; + boolean texEnabled; + int texUnit; + int oldProgram; + int oldArrayBuffer; + } + + private RenderState startRender() { + RenderState state = new RenderState(); + state.smoothedPoly = GL11.glGetBoolean(GL11.GL_POLYGON_SMOOTH); + state.blendEnabled = GL11.glGetBoolean(GL11.GL_BLEND); + state.depthEnabled = GL11.glGetBoolean(GL11.GL_DEPTH_TEST); + state.depthWriteEnabled = GL11.glGetBoolean(GL11.GL_DEPTH_WRITEMASK); + state.texEnabled = GL11.glGetBoolean(GL11.GL_TEXTURE_2D); + state.texUnit = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE); + state.oldProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM); + state.oldArrayBuffer = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING); + GL11.glDisable(GL11.GL_POLYGON_SMOOTH); + GL11.glEnable(GL11.GL_BLEND); + GL14.glBlendEquation(GL14.GL_FUNC_ADD); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glEnable(GL11.GL_DEPTH_TEST); + GL11.glDepthMask(true); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_TEXTURE_1D); + GL11.glBindTexture(GL11.GL_TEXTURE_1D, staticState.gradientTexture); + GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP); + + GL20.glUseProgram(0); + + GL11.glMatrixMode(GL11.GL_PROJECTION); + GL11.glPushMatrix(); + GL11.glLoadIdentity(); + GL11.glMatrixMode(GL11.GL_MODELVIEW); + GL11.glPushMatrix(); + GL11.glLoadIdentity(); + + return state; + } + + private void endRender(RenderState state) { + GL11.glMatrixMode(GL11.GL_PROJECTION); + GL11.glPopMatrix(); + GL11.glMatrixMode(GL11.GL_MODELVIEW); + GL11.glPopMatrix(); + GL11.glEnable(GL11.GL_BLEND); + GL20.glUseProgram(state.oldProgram); + GL13.glActiveTexture(state.texUnit); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, state.oldArrayBuffer); + if (!state.depthWriteEnabled) { + GL11.glDepthMask(false); + } + if (!state.depthEnabled) { + GL11.glDisable(GL11.GL_DEPTH_TEST); + } + if (state.texEnabled) { + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + if (state.smoothedPoly) { + GL11.glEnable(GL11.GL_POLYGON_SMOOTH); + } + if (!state.blendEnabled) { + GL11.glDisable(GL11.GL_BLEND); + } + } + + /** + * Do the actual drawing of the curve into the currently bound framebuffer. + * @param color the color of the curve + * @param curve the points along the curve + */ + private void draw_curve(Color color, Vec2f[] curve) { + RenderState state = startRender(); + int vtx_buf; + //floatsize * (position + texture coordinates) * (number of cones) * (vertices in a cone) + FloatBuffer buff = BufferUtils.createByteBuffer(4 * (4 + 2) * (2 * curve.length - 1) * (NewCurveStyleState.DIVIDES + 2)).asFloatBuffer(); + staticState.initShaderProgram(); + staticState.initGradient(); + vtx_buf = GL15.glGenBuffers(); + for (int i = 0; i < curve.length; ++i) { + float x = curve[i].x; + float y = curve[i].y; + //if(i == 0 || i==curve.length-1){ + fillCone(buff, x, y, NewCurveStyleState.DIVIDES); + if (i != 0) { + float last_x = curve[i - 1].x; + float last_y = curve[i - 1].y; + double diff_x = x - last_x; + double diff_y = y - last_y; + x = (float) (x - diff_x / 2); + y = (float) (y - diff_y / 2); + fillCone(buff, x, y, NewCurveStyleState.DIVIDES); + } + } + buff.flip(); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vtx_buf); + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buff, GL15.GL_STATIC_DRAW); + staticState.render(color, curve.length * 2 - 1, vtx_buf); + GL15.glDeleteBuffers(vtx_buf); + endRender(state); + } + + /** + * Fill {@code buff} with the texture coordinates and positions for a cone + * with {@code DIVIDES} ground corners that has its center at the coordinates + * {@code (x1,y1)} + * @param buff the buffer to be filled + * @param x1 x-coordinate of the cone + * @param y1 y-coordinate of the cone + * @param DIVIDES the base of the cone is a regular polygon with this many sides + */ + protected void fillCone(FloatBuffer buff, float x1, float y1, final int DIVIDES) { + float divx = Options.getLatestResolutionWidth() / 2.0f; + float divy = Options.getLatestResolutionHeight() / 2.0f; + float offx = -1.0f; + float offy = 1.0f; + float x, y; + float radius = scale / 2; + buff.put(1.0f); + buff.put(0.5f); + //GL11.glTexCoord2d(1.0, 0.5); + x = offx + x1 / divx; + y = offy - y1 / divy; + buff.put(x); + buff.put(y); + buff.put(0f); + buff.put(1f); + //GL11.glVertex4f(x, y, 0.0f, 1.0f); + for (int j = 0; j < DIVIDES; ++j) { + double phase = j * (float) Math.PI * 2 / DIVIDES; + buff.put(0.0f); + buff.put(0.5f); + //GL11.glTexCoord2d(0.0, 0.5); + x = (x1 + radius * (float) Math.sin(phase)) / divx; + y = (y1 + radius * (float) Math.cos(phase)) / divy; + buff.put((offx + x)); + buff.put((offy - y)); + buff.put(1f); + buff.put(1f); + //GL11.glVertex4f(x + 90 * (float) Math.sin(phase), y + 90 * (float) Math.cos(phase), 1.0f, 1.0f); + } + buff.put(0.0f); + buff.put(0.5f); + //GL11.glTexCoord2d(0.0, 0.5); + x = (x1 + radius * (float) Math.sin(0.0)) / divx; + y = (y1 + radius * (float) Math.cos(0.0)) / divy; + buff.put((offx + x)); + buff.put((offy - y)); + buff.put(1f); + buff.put(1f); + //GL11.glVertex4f(x + 90 * (float) Math.sin(0.0), y + 90 * (float) Math.cos(0.0), 1.0f, 1.0f); + } + + /** + * Contains all the necessary state that needs to be tracked to draw curves + * in the new style and not re-create the shader each time. + * + * @author Bigpet {@literal } + */ + private static class NewCurveStyleState { + + /** + * used for new style Slider rendering, defines how many vertices the + * base of the cone has that is used to draw the curve + */ + protected static final int DIVIDES = 30; + + /** + * OpenGL shader program ID used to draw and recolor the curve + */ + protected int program = 0; + + /** + * OpenGL shader attribute location of the vertex position attribute + */ + protected int attribLoc = 0; + + /** + * OpenGL shader attribute location of the texture coordinate attribute + */ + protected int texCoordLoc = 0; + + /** + * OpenGL shader uniform location of the color attribute + */ + protected int colLoc = 0; + + /** + * OpenGL texture id for the gradient texture for the curve + */ + protected int gradientTexture = 0; + + /** + * Reads the first row of the slider gradient texture and upload it as + * a 1D texture to OpenGL if it hasn't already been done + */ + public void initGradient() + { + if(gradientTexture == 0) + { + Image slider = GameImage.SLIDER_GRADIENT.getImage(); + staticState.gradientTexture = GL11.glGenTextures(); + ByteBuffer buff = BufferUtils.createByteBuffer(slider.getWidth()*4); + for(int i=0 ; i< slider.getWidth(); ++i) + { + Color col = slider.getColor(i, 0); + buff.put((byte)(255*col.r)); + buff.put((byte)(255*col.g)); + buff.put((byte)(255*col.b)); + buff.put((byte)(255*col.a)); + + } + buff.flip(); + GL11.glBindTexture(GL11.GL_TEXTURE_1D, gradientTexture); + GL11.glTexImage1D(GL11.GL_TEXTURE_1D,0,GL11.GL_RGBA,slider.getWidth(),0,GL11.GL_RGBA,GL11.GL_UNSIGNED_BYTE,buff); + GL30.glGenerateMipmap(GL11.GL_TEXTURE_1D); + } + } + + /** + * Compiles and links the shader program for the new style curve objects + * if it hasn't already been compiled and linked. + */ + public void initShaderProgram() { + if (program == 0) { + program = GL20.glCreateProgram(); + int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER); + int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER); + GL20.glShaderSource(vtxShdr, "#version 330\n" + + "\n" + + "layout(location = 0) in vec4 in_position;\n" + + "layout(location = 1) in vec2 in_tex_coord;\n" + + "\n" + + "out vec2 tex_coord;\n" + + "void main()\n" + + "{\n" + + " gl_Position = in_position;\n" + + " tex_coord = in_tex_coord;\n" + + "}"); + GL20.glCompileShader(vtxShdr); + int res = GL20.glGetShaderi(vtxShdr, GL20.GL_COMPILE_STATUS); + if (res != GL11.GL_TRUE) { + String error = GL20.glGetShaderInfoLog(vtxShdr, 1024); + Log.error("Vertex Shader compilation failed", new Exception(error)); + } + GL20.glShaderSource(frgShdr, "#version 330\n" + + "\n" + + "uniform sampler1D tex;\n" + + "uniform vec2 tex_size;\n" + + "uniform vec3 col_tint;\n" + + "\n" + + "in vec2 tex_coord;\n" + + "layout(location = 0) out vec4 out_colour;\n" + + "\n" + + "void main()\n" + + "{\n" + + " vec4 in_color = texture(tex, tex_coord.x);\n" + + " float blend_factor = in_color.r-in_color.b;\n" + + " vec4 new_color = vec4(mix(in_color.xyz,col_tint,blend_factor),in_color.w);\n" + + " out_colour = new_color;\n" + + "}"); + GL20.glCompileShader(frgShdr); + res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS); + if (res != GL11.GL_TRUE) { + String error = GL20.glGetShaderInfoLog(frgShdr, 1024); + Log.error("Fragment Shader compilation failed", new Exception(error)); + } + GL20.glAttachShader(program, vtxShdr); + GL20.glAttachShader(program, frgShdr); + GL20.glLinkProgram(program); + res = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS); + if (res != GL11.GL_TRUE) { + String error = GL20.glGetProgramInfoLog(program, 1024); + Log.error("Program linking failed", new Exception(error)); + } + attribLoc = GL20.glGetAttribLocation(program, "in_position"); + texCoordLoc = GL20.glGetAttribLocation(program, "in_tex_coord"); + int texLoc = GL20.glGetUniformLocation(program, "tex"); + colLoc = GL20.glGetUniformLocation(program, "col_tint"); + GL20.glUniform1i(texLoc, 0); + } + } + + /** + * Make the drawcalls for the curve. This requires the {@code vertexBufferObject} to be filled with + * {@code numCones * (NewCurveStyleState.DIVIDES+2)} sets of (u,v,x,y,z,w) floats + * @param color the color of the curve to be drawn + * @param numCones the number of points in the {code vertexBufferObject} + * @param vertexBufferObject the OpenGL vertex buffer object ID that + * contains the positions and texture coordinates of the cones + */ + public void render(Color color, int numCones, int vertexBufferObject) { + GL20.glUseProgram(program); + GL20.glEnableVertexAttribArray(attribLoc); + GL20.glEnableVertexAttribArray(texCoordLoc); + //stride is 6*4 for the floats (4 bytes) (u,v)(x,y,z,w) + //2*4 is for skipping the first 2 floats (u,v) + GL20.glVertexAttribPointer(attribLoc, 4, GL11.GL_FLOAT, false, 6 * 4, 2 * 4); + GL20.glVertexAttribPointer(texCoordLoc, 2, GL11.GL_FLOAT, false, 6 * 4, 0); + + GL20.glUniform3f(colLoc, color.r, color.g, color.b); + for (int i = 0; i < numCones; ++i) { + GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2); + } + + GL20.glDisableVertexAttribArray(texCoordLoc); + GL20.glDisableVertexAttribArray(attribLoc); + } + + } + +} diff --git a/src/itdelatrisu/opsu/render/FrameBufferCache.java b/src/itdelatrisu/opsu/render/FrameBufferCache.java new file mode 100644 index 00000000..f3a447b4 --- /dev/null +++ b/src/itdelatrisu/opsu/render/FrameBufferCache.java @@ -0,0 +1,123 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ +package itdelatrisu.opsu.render; + +import itdelatrisu.opsu.Options; +import itdelatrisu.opsu.beatmap.HitObject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import org.newdawn.slick.util.Log; + +/** + * This is cache for OpenGL FrameBufferObjects. This is currently only used + * to draw curve objects of the new slider style. Does currently not integrate + * well and requires some manual OpenGL state manipulation to use it. + * @author Bigpet {@literal } + */ +public class FrameBufferCache { + private static final int INITIAL_CACHE_SIZE = 4; + private static FrameBufferCache instance = null; + private Map cacheMap; + private ArrayList cache; + public final int width; + public final int height; + + private FrameBufferCache(int width, int height) { + cache = new ArrayList<>(INITIAL_CACHE_SIZE); + cacheMap = new HashMap<>(); + this.width = width; + this.height = height; + for (int i = 0; i < INITIAL_CACHE_SIZE; ++i) { + cache.add(Rendertarget.createRTTFramebuffer(width, height)); + } + } + + /** + * Check if there is a framebuffer object mapped to the {@code HitObject obj} + * @param obj + * @return true if there is a framebuffer mapped for this HitObject, else false + */ + public boolean contains(HitObject obj) { + return cacheMap.containsKey(obj); + } + + /** + * Get the {@code Rendertarget} mapped to {@code obj} + * @param obj + * @return the {@code Rendertarget} if there's one mapped to {@code obj}, otherwise null + */ + public Rendertarget get(HitObject obj) { + return cacheMap.get(obj); + } + + /** + * Clear the mapping for {@code obj} to free it up to get used by another {@code HitObject} + * @param obj + * @return true if there was a mapping for {@code obj} and false if there was no mapping for it. + */ + public boolean freeMappingFor(HitObject obj) { + return cacheMap.remove(obj) != null; + } + + /** + * Clear the cache of all the mappings. This does not actually delete the + * cached framebuffers, it merely frees them all up to get mapped anew. + */ + public void freeMap() { + cacheMap.clear(); + } + + /** + * Create a mapping from {@code obj} to a framebuffer. If there was already + * a mapping from {@code obj} this will associate another framebuffer with it + * (thereby freeing up the previously mapped framebuffer). + * @param obj + * @return the {@code Rendertarget} newly mapped to {@code obj} + */ + public Rendertarget insert(HitObject obj) { + //find first RTTFramebuffer that's not mapped to anything and return it + Rendertarget buffer; + for (int i = 0; i < cache.size(); ++i) { + buffer = cache.get(i); + if (!cacheMap.containsValue(buffer)) { + cacheMap.put(obj, buffer); + return buffer; + } + } + //no unmapped RTTFramebuffer found, create a new one + buffer = Rendertarget.createRTTFramebuffer(width, height); + cache.add(buffer); + Log.warn("Framebuffer cache was not large enough, possible resource leak."); + cacheMap.put(obj, buffer); + return buffer; + } + + /** + * There should only ever be one framebuffer cache, this function returns that one framebuffer cache instance. + * If there was no instance created already then this function creates it. + * @return the instance of FrameBufferCache + */ + public static FrameBufferCache getInstance() { + if (instance == null) { + instance = new FrameBufferCache(Options.getLatestResolutionWidth(), Options.getLatestResolutionHeight()); + } + return instance; + } + +} diff --git a/src/itdelatrisu/opsu/render/Rendertarget.java b/src/itdelatrisu/opsu/render/Rendertarget.java new file mode 100644 index 00000000..c90158fe --- /dev/null +++ b/src/itdelatrisu/opsu/render/Rendertarget.java @@ -0,0 +1,110 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * opsu! is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with opsu!. If not, see . + */ +package itdelatrisu.opsu.render; + +import java.nio.ByteBuffer; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL32; + +/** + * + * Represents a rendertarget. For now this maps to an OpenGL FBO via LWJGL + */ +public class Rendertarget { + public final int width; + public final int height; + private final int fboID; + private final int textureID; + + /** + * Create a new FBO + * + * @param width + * @param height + */ + private Rendertarget(int width, int height) + { + this.width = width; + this.height = height; + fboID = GL30.glGenFramebuffers(); + textureID = GL11.glGenTextures(); + } + + /** + * Bind this rendertarget as the primary framebuffer. + */ + public void bind() + { + GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fboID); + } + + //use judiciously, try to avoid if possible and consider adding a method to + //this class if you find yourself calling this repeatedly + public int getID() + { + return fboID; + } + + //try not to use, could be moved into seperate class + public int getTextureID() + { + return textureID; + } + + /** + * Bind the default framebuffer + */ + public static void unbind() + { + GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, 0); + } + + /** + * Creates a Rendertarget with a Texture that it renders the color buffer in + * and a renderbuffer that it renders the depth to. + */ + public static Rendertarget createRTTFramebuffer(int width, int height) + { + int old_framebuffer = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING); + int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + Rendertarget buffer = new Rendertarget(width,height); + buffer.bind(); + + int fboTexture = buffer.textureID; + GL11.glBindTexture(GL11.GL_TEXTURE_2D, fboTexture); + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, 4, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_INT, (ByteBuffer) null); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + + int fboDepth = GL30.glGenRenderbuffers(); + GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, fboDepth); + GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT, width, height); + GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, fboDepth); + + GL32.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, fboTexture, 0); + GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0); + + GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture); + GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_framebuffer); + + + return buffer; + } +} diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 01e840c5..ff6d14cb 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -41,6 +41,7 @@ import itdelatrisu.opsu.objects.DummyObject; import itdelatrisu.opsu.objects.GameObject; import itdelatrisu.opsu.objects.Slider; import itdelatrisu.opsu.objects.Spinner; +import itdelatrisu.opsu.render.FrameBufferCache; import itdelatrisu.opsu.replay.PlaybackSpeed; import itdelatrisu.opsu.replay.Replay; import itdelatrisu.opsu.replay.ReplayFrame; @@ -1023,6 +1024,9 @@ public class Game extends BasicGameState { if (beatmap == null || beatmap.objects == null) throw new RuntimeException("Running game with no beatmap loaded."); + //@TODO: find a better place to clean the SliderCache + FrameBufferCache.getInstance().freeMap(); + // grab the mouse (not working for touchscreen) // container.setMouseGrabbed(true); diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index d1b3003e..724fd3da 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -59,7 +59,8 @@ public class OptionsMenu extends BasicGameState { GameOption.SCREENSHOT_FORMAT, GameOption.NEW_CURSOR, GameOption.DYNAMIC_BACKGROUND, - GameOption.LOAD_VERBOSE + GameOption.LOAD_VERBOSE, + GameOption.NEW_SLIDER }), MUSIC ("Music", new GameOption[] { GameOption.MASTER_VOLUME,