diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 1dd3de06..ebc3a28c 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -907,7 +907,7 @@ public class GameData { float oldColorAlpha = hitResult.color.a; Colors.WHITE_FADE.a = alpha; hitResult.color.a = alpha; - hitResult.curve.draw(hitResult.color); + hitResult.curve.draw(hitResult.color,1.0f); Colors.WHITE_FADE.a = oldWhiteAlpha; hitResult.color.a = oldColorAlpha; } diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 18d701bd..5c5c4416 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -417,6 +417,7 @@ public class Options { BACKGROUND_DIM ("Background Dim", "DimLevel", "Percentage to dim the background image during gameplay.", 50, 0, 100), FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "ForceDefaultPlayfield", "Override the song background with the default playfield background.", false), IGNORE_BEATMAP_SKINS ("Ignore All Beatmap Skins", "IgnoreBeatmapSkins", "Never use skin element overrides provided by beatmaps.", false), + SNAKING_SLIDERS ("Snaking sliders", "SnakingSliders", "Sliders gradually snake out from their starting point.", true), SHOW_HIT_LIGHTING ("Show Hit Lighting", "HitLighting", "Adds an effect behind hit explosions.", true), SHOW_COMBO_BURSTS ("Show Combo Bursts", "ComboBurst", "A character image is displayed at combo milestones.", true), SHOW_PERFECT_HIT ("Show Perfect Hits", "PerfectHit", "Whether to show perfect hit result bursts (300s, slider ticks).", true), @@ -941,6 +942,12 @@ public class Options { */ public static boolean isBeatmapSkinIgnored() { return GameOption.IGNORE_BEATMAP_SKINS.getBooleanValue(); } + /** + * Returns whether or not sliders should snake in or just appear fully at once. + * @return true if sliders should snake in + */ + public static boolean isSliderSnaking() { return GameOption.SNAKING_SLIDERS.getBooleanValue(); } + /** * Returns the fixed circle size override, if any. * @return the CS value (0, 10], 0f if disabled diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 2d2476b4..7c394276 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -30,6 +30,7 @@ import itdelatrisu.opsu.objects.curves.Curve; import itdelatrisu.opsu.objects.curves.Vec2f; import itdelatrisu.opsu.states.Game; import itdelatrisu.opsu.ui.Colors; +import itdelatrisu.opsu.ui.animations.AnimationEquation; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; @@ -179,20 +180,27 @@ public class Slider implements GameObject { float approachScale = 1 + scale * 3; float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime; float alpha = Utils.clamp(1 - fadeinScale, 0, 1); + float decorationsAlpha = Utils.clamp(-2.0f * fadeinScale, 0, 1); boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber(); - float oldAlpha = Colors.WHITE_FADE.a; Colors.WHITE_FADE.a = color.a = alpha; Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); Image hitCircle = GameImage.HITCIRCLE.getImage(); Vec2f endPos = curve.pointAt(1); - curve.draw(color); + float curveInterval; + if(Options.isSliderSnaking()){ + curveInterval = alpha; + } else { + curveInterval = 1.0f; + } + curve.draw(color,curveInterval); color.a = alpha; // end circle - hitCircle.drawCentered(endPos.x, endPos.y, color); - hitCircleOverlay.drawCentered(endPos.x, endPos.y, Colors.WHITE_FADE); + Vec2f endCircPos = curve.pointAt(curveInterval); + hitCircle.drawCentered(endCircPos.x, endCircPos.y, color); + hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, Colors.WHITE_FADE); // start circle hitCircle.drawCentered(x, y, color); @@ -201,10 +209,13 @@ public class Slider implements GameObject { // ticks if (ticksT != null) { - Image tick = GameImage.SLIDER_TICK.getImage(); + float tickScale = 0.5f + 0.5f*AnimationEquation.OUT_BACK.calc(decorationsAlpha); + Image tick = GameImage.SLIDER_TICK.getImage().getScaledCopy(tickScale); for (int i = 0; i < ticksT.length; i++) { Vec2f c = curve.pointAt(ticksT[i]); - tick.drawCentered(c.x, c.y, Colors.WHITE_FADE); + Colors.WHITE_FADE.a = decorationsAlpha; + tick.drawCentered(c.x , c.y , Colors.WHITE_FADE); + Colors.WHITE_FADE.a = alpha; } } if (GameMod.HIDDEN.isActive()) { @@ -224,24 +235,32 @@ public class Slider implements GameObject { hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE); // repeats - for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) { - if (hitObject.getRepeatCount() - 1 > tcurRepeat) { - Image arrow = GameImage.REVERSEARROW.getImage(); - if (tcurRepeat != currentRepeats) { - if (sliderTime == 0) - continue; - float t = Math.max(getT(trackPosition, true), 0); - arrow.setAlpha((float) (t - Math.floor(t))); - } else - arrow.setAlpha(1f); - if (tcurRepeat % 2 == 0) { - // last circle - arrow.setRotation(curve.getEndAngle()); - arrow.drawCentered(endPos.x, endPos.y); - } else { - // first circle - arrow.setRotation(curve.getStartAngle()); - arrow.drawCentered(x, y); + if (curveInterval == 1.0f) { + for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) { + if (hitObject.getRepeatCount() - 1 > tcurRepeat) { + Image arrow = GameImage.REVERSEARROW.getImage(); + if (tcurRepeat != currentRepeats) { + if (sliderTime == 0) { + continue; + } + float t = Math.max(getT(trackPosition, true), 0); + arrow.setAlpha((float) (t - Math.floor(t))); + } else { + if(Options.isSliderSnaking()){ + arrow.setAlpha(decorationsAlpha); + } else { + arrow.setAlpha(1f); + } + } + if (tcurRepeat % 2 == 0) { + // last circle + arrow.setRotation(curve.getEndAngle()); + arrow.drawCentered(endPos.x, endPos.y); + } else { + // first circle + arrow.setRotation(curve.getStartAngle()); + arrow.drawCentered(x, y); + } } } } diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index 313013db..aab35343 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -20,6 +20,7 @@ package itdelatrisu.opsu.objects.curves; import itdelatrisu.opsu.GameImage; import itdelatrisu.opsu.Options; +import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.render.CurveRenderState; import itdelatrisu.opsu.skins.Skin; @@ -111,28 +112,31 @@ public abstract class Curve { public abstract Vec2f pointAt(float t); /** - * Draws the full curve to the graphics context. + * Draws the curve in the range [0, t] (where the full range is [0, 1]) to the graphics context. * @param color the color filter + * @param t set the curve interval to [0, t] */ - public void draw(Color color) { + public void draw(Color color, float t) { if (curve == null) return; + t = Utils.clamp(t, 0.0f, 1.0f); // peppysliders if (Options.getSkin().getSliderStyle() == Skin.STYLE_PEPPYSLIDER || !mmsliderSupported) { + int drawUpTo = (int)(curve.length*t); Image hitCircle = GameImage.HITCIRCLE.getImage(); Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage(); - for (int i = 0; i < curve.length; i++) + for (int i = 0; i < drawUpTo; i++) hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Colors.WHITE_FADE); - for (int i = 0; i < curve.length; i++) + for (int i = 0; i < drawUpTo; i++) hitCircle.drawCentered(curve[i].x, curve[i].y, color); } // mmsliders else { if (renderState == null) - renderState = new CurveRenderState(hitObject); - renderState.draw(color, borderColor, curve); + renderState = new CurveRenderState(hitObject,curve); + renderState.draw(color, borderColor, t); } } diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index 2d1ac1ad..42baab40 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -18,9 +18,9 @@ package itdelatrisu.opsu.render; import itdelatrisu.opsu.GameImage; +import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.objects.curves.Vec2f; -import itdelatrisu.opsu.ui.Colors; import java.nio.ByteBuffer; import java.nio.FloatBuffer; @@ -59,6 +59,12 @@ public class CurveRenderState { /** The HitObject associated with the curve to be drawn. */ protected HitObject hitObject; + /** the points along the curve to be drawn */ + protected Vec2f[] curve; + + /** The point to which the curve has last been rendered into the texture (as an index into {@code curve}) */ + private int lastPointDrawn; + /** * Set the width and height of the container that Curves get drawn into. * Should be called before any curves are drawn. @@ -74,11 +80,12 @@ public class CurveRenderState { scale = (int) (circleDiameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480) //scale = scale * 118 / 128; //for curves exactly as big as the sliderball FrameBufferCache.init(width, height); + NewCurveStyleState.initUnitCone(); } /** * Undo the static state. Static state setup caused by calls to - * {@link #draw(org.newdawn.slick.Color, org.newdawn.slick.Color, itdelatrisu.opsu.objects.curves.Vec2f[])} + * {@link #draw(org.newdawn.slick.Color, org.newdawn.slick.Color, float)} * are undone. */ public static void shutdown() { @@ -89,10 +96,12 @@ public class CurveRenderState { /** * Creates an object to hold the render state that's necessary to draw a curve. * @param hitObject the HitObject that represents this curve, just used as a unique ID + * @param curve the points along the curve to be drawn */ - public CurveRenderState(HitObject hitObject) { + public CurveRenderState(HitObject hitObject, Vec2f[] curve) { fbo = null; this.hitObject = hitObject; + this.curve = curve; } /** @@ -101,9 +110,10 @@ public class CurveRenderState { * runs it just draws the cached copy to the screen. * @param color tint of the curve * @param borderColor the curve border color - * @param curve the points along the curve to be drawn + * @param t the point up to which the curve should be drawn (in the interval [0, 1]) */ - public void draw(Color color, Color borderColor, Vec2f[] curve) { + public void draw(Color color, Color borderColor, float t) { + t = Utils.clamp(t, 0.0f, 1.0f); float alpha = color.a; // if this curve hasn't been drawn, draw it and cache the result @@ -113,26 +123,40 @@ public class CurveRenderState { if (mapping == null) mapping = cache.insert(hitObject); fbo = mapping; + createVertexBuffer(fbo.getVbo()); + //write impossible value to make sure the fbo is cleared + lastPointDrawn = -1; + } + + int drawUpTo = (int) (t * curve.length); + + if (lastPointDrawn != drawUpTo) { + + if (drawUpTo == lastPointDrawn) { + return; + } int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); - //glGetInteger requires a buffer of size 16, even though just 4 //values are returned in this specific case IntBuffer oldViewport = BufferUtils.createIntBuffer(16); GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport); EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 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); - Colors.WHITE_FADE.a = 1.0f; - this.draw_curve(color, borderColor, curve); + if (lastPointDrawn <= 0 || lastPointDrawn > drawUpTo) { + lastPointDrawn = 0; + GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + } + + this.renderCurve(color, borderColor, lastPointDrawn, drawUpTo); + lastPointDrawn = drawUpTo; color.a = 1f; GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex); EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb); GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3)); - Colors.WHITE_FADE.a = alpha; } // draw a fullscreen quad with the texture that contains the curve @@ -180,7 +204,7 @@ public class CurveRenderState { * Backup the current state of the relevant OpenGL state and change it to * what's needed to draw the curve. */ - private RenderState startRender() { + private RenderState saveRenderState() { RenderState state = new RenderState(); state.smoothedPoly = GL11.glGetBoolean(GL11.GL_POLYGON_SMOOTH); state.blendEnabled = GL11.glGetBoolean(GL11.GL_BLEND); @@ -219,7 +243,7 @@ public class CurveRenderState { * Restore the old OpenGL state that's backed up in {@code state}. * @param state the old state to restore */ - private void endRender(RenderState state) { + private void restoreRenderState(RenderState state) { GL11.glMatrixMode(GL11.GL_PROJECTION); GL11.glPopMatrix(); GL11.glMatrixMode(GL11.GL_MODELVIEW); @@ -241,24 +265,18 @@ public class CurveRenderState { } /** - * Do the actual drawing of the curve into the currently bound framebuffer. - * @param color the color of the curve - * @param borderColor the curve border color - * @param curve the points along the curve + * Write the vertices and (with position and texture coordinates) for the full + * curve into the OpenGL buffer with the ID specified by {@code bufferID} + * @param bufferID the buffer ID for the OpenGL buffer the vertices should be written into */ - private void draw_curve(Color color, Color borderColor, Vec2f[] curve) { - staticState.initGradient(); - RenderState state = startRender(); - int vtx_buf; - // the size is: floatsize * (position + texture coordinates) * (number of cones) * (vertices in a cone) + private void createVertexBuffer(int bufferID) + { + int arrayBufferBinding = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING); FloatBuffer buff = BufferUtils.createByteBuffer(4 * (4 + 2) * (2 * curve.length - 1) * (NewCurveStyleState.DIVIDES + 2)).asFloatBuffer(); - staticState.initShaderProgram(); - 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); + fillCone(buff, x, y); if (i != 0) { float last_x = curve[i - 1].x; float last_y = curve[i - 1].y; @@ -266,12 +284,25 @@ public class CurveRenderState { double diff_y = y - last_y; x = (float) (x - diff_x / 2); y = (float) (y - diff_y / 2); - fillCone(buff, x, y, NewCurveStyleState.DIVIDES); + fillCone(buff, x, y); } } buff.flip(); - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vtx_buf); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferID); GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buff, GL15.GL_STATIC_DRAW); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, arrayBufferBinding); + } + + /** + * Do the actual drawing of the curve into the currently bound framebuffer. + * @param color the color of the curve + * @param borderColor the curve border color + */ + private void renderCurve(Color color, Color borderColor, int from, int to) { + staticState.initGradient(); + RenderState state = saveRenderState(); + staticState.initShaderProgram(); + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, fbo.getVbo()); GL20.glUseProgram(staticState.program); GL20.glEnableVertexAttribArray(staticState.attribLoc); GL20.glEnableVertexAttribArray(staticState.texCoordLoc); @@ -282,63 +313,37 @@ public class CurveRenderState { //2*4 is for skipping the first 2 floats (u,v) GL20.glVertexAttribPointer(staticState.attribLoc, 4, GL11.GL_FLOAT, false, 6 * 4, 2 * 4); GL20.glVertexAttribPointer(staticState.texCoordLoc, 2, GL11.GL_FLOAT, false, 6 * 4, 0); - for (int i = 0; i < curve.length * 2 - 1; ++i) + for (int i = from*2; i < to * 2 - 1; ++i) GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2); + GL11.glFlush(); GL20.glDisableVertexAttribArray(staticState.texCoordLoc); GL20.glDisableVertexAttribArray(staticState.attribLoc); - GL15.glDeleteBuffers(vtx_buf); - endRender(state); + restoreRenderState(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)}. + * 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) { + protected void fillCone(FloatBuffer buff, float x1, float y1) { float divx = containerWidth / 2.0f; float divy = containerHeight / 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); + + for(int i = 0; i