From 131138ea8c4f7112ff84b501beba0d287e303afc Mon Sep 17 00:00:00 2001 From: Peter Tissen Date: Wed, 16 Sep 2015 17:19:23 +0200 Subject: [PATCH] Snaking sliders initial implementation. Draws curves in a range from 0 to x (where x is a value between 0 and 1) while blending them in (if enabled in the options) Cache a unit-cone once and re-use that with scaling and translation instead of generating it each time again. This is more for readability than performance (since presumably feeding it mostly constants like before would be faster) Only draws the cones in the curve once if possible (only possible if consective calls to draw with the intervals x_1 before x_2 are made and [0, x_1] [0, x_2] with x_1 < x_2 holds true) minor refactoring and cleanup Omg my code is such a mess. I feel like I've committed a crime against humanity by just randomly putting that random vbo id into the class called "Rendertarget". But there's really no good place for it now (that has a way to clean it up). But if "Rendertarget" will ever be used by anything else but the sliders I'm gonna pull that out. added blending in for return arrows and ticks --- src/itdelatrisu/opsu/GameData.java | 2 +- src/itdelatrisu/opsu/Options.java | 7 + src/itdelatrisu/opsu/objects/Slider.java | 67 ++++--- .../opsu/objects/curves/Curve.java | 16 +- .../opsu/render/CurveRenderState.java | 189 +++++++++++------- src/itdelatrisu/opsu/render/Rendertarget.java | 25 ++- src/itdelatrisu/opsu/states/OptionsMenu.java | 1 + 7 files changed, 203 insertions(+), 104 deletions(-) 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