diff --git a/res/slidergradient_ex.png b/res/slidergradient_ex.png new file mode 100644 index 00000000..363fa418 Binary files /dev/null and b/res/slidergradient_ex.png differ diff --git a/src/itdelatrisu/opsu/Container.java b/src/itdelatrisu/opsu/Container.java index 617386f7..a95d34a8 100644 --- a/src/itdelatrisu/opsu/Container.java +++ b/src/itdelatrisu/opsu/Container.java @@ -29,6 +29,7 @@ import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.options.Options; import itdelatrisu.opsu.render.CurveRenderState; +import itdelatrisu.opsu.render.LegacyCurveRenderState; import itdelatrisu.opsu.ui.UI; import org.lwjgl.opengl.Display; @@ -179,6 +180,7 @@ private void close_sub() { // delete OpenGL objects involved in the Curve rendering CurveRenderState.shutdown(); + LegacyCurveRenderState.shutdown(); // destroy watch service if (!Options.isWatchServiceEnabled()) diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 842cf42e..619406eb 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -1088,14 +1088,18 @@ else if (Options.isHitLightingEnabled() && !hitResult.hideResult && hitResult.re */ private void drawHitAnimations(HitObjectResult hitResult, int trackPosition) { // fade out slider curve - if (hitResult.result != HIT_SLIDER_REPEAT && hitResult.curve != null) { + if (hitResult.result != HIT_SLIDER_REPEAT && hitResult.curve != null && !(Options.isExperimentalSliderStyle() && Options.isShrinkingSliders())) { float progress = AnimationEquation.OUT_CUBIC.calc( (float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME); float alpha = 1f - progress; float oldWhiteAlpha = Colors.WHITE_FADE.a; float oldColorAlpha = hitResult.color.a; Colors.WHITE_FADE.a = hitResult.color.a = alpha; - hitResult.curve.draw(hitResult.color); + if (Options.isExperimentalSliderStyle()) { + hitResult.curve.draw(hitResult.color, Options.isMergingSliders() ? 1 : 0, hitResult.curve.getCurvePoints().length); + } else { + hitResult.curve.draw(hitResult.color); + } Colors.WHITE_FADE.a = oldWhiteAlpha; hitResult.color.a = oldColorAlpha; } @@ -1125,6 +1129,10 @@ private void drawHitAnimations(HitObjectResult hitResult, int trackPosition) { fc.drawCentered(hitResult.x, hitResult.y); } + if (hitResult.result != HIT_SLIDER_REPEAT && hitResult.curve != null && !Options.isDrawSliderEndCircles()) { + return; + } + // hit circles float progress = AnimationEquation.OUT_CUBIC.calc( (float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME); diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index deb5e402..49ed118b 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -105,6 +105,7 @@ protected Image process_sub(Image img, int w, int h) { // Slider SLIDER_GRADIENT ("slidergradient", "png"), + SLIDER_GRADIENT_EX ("slidergradient_ex", "png"), SLIDER_BALL ("sliderb", "sliderb%d", "png"), SLIDER_FOLLOWCIRCLE ("sliderfollowcircle", "png"), REVERSEARROW ("reversearrow", "png"), diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 209d82a7..f4b816e2 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -235,6 +235,21 @@ public static float clamp(float val, float low, float high) { return val; } + /** + * Clamps a value between a lower and upper bound. + * @param val the value to clamp + * @param low the lower bound + * @param high the upper bound + * @return the clamped value + */ + public static double clamp(double val, double low, double high) { + if (val < low) + return low; + if (val > high) + return high; + return val; + } + /** * Returns the distance between two points. * @param x1 the x-component of the first point diff --git a/src/itdelatrisu/opsu/beatmap/HitObject.java b/src/itdelatrisu/opsu/beatmap/HitObject.java index ca5f5790..07be1b5a 100644 --- a/src/itdelatrisu/opsu/beatmap/HitObject.java +++ b/src/itdelatrisu/opsu/beatmap/HitObject.java @@ -271,6 +271,19 @@ else if ((type & HitObject.TYPE_SLIDER) > 0) { } } + /** + * Constructor to make fake/temp hitobj + * @param x xpos + * @param y ypos + * @param time time + */ + public HitObject( float x, float y, int time) { + this.x = x; + this.y = y; + this.time = time; + this.type = HitObject.TYPE_CIRCLE; + } + /** * Returns the raw starting x coordinate. */ diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index 05e29237..14946983 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -38,7 +38,7 @@ */ public class Circle implements GameObject { /** The diameter of hit circles. */ - private static float diameter; + public static float diameter; /** The associated HitObject. */ private HitObject hitObject; diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 96a75acd..25ed5f41 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -117,6 +117,12 @@ public class Slider implements GameObject { /** Container dimensions. */ private static int containerWidth, containerHeight; + /** Curve color for experimental style sliders. */ + private static Color curveColor = new Color(0, 0, 0, 20); + + /** Start index of this slider in the merged slider. */ + public int baseSliderFrom; + /** * Initializes the Slider data type with images and dimensions. * @param container the game container @@ -212,11 +218,18 @@ public void draw(Graphics g, int trackPosition) { Math.max(0f, 1f - ((float) (trackPosition - hitObject.getTime()) / (getEndTime() - hitObject.getTime())) * 1.05f); } - float curveInterval = Options.isSliderSnaking() ? alpha : 1f; - curve.draw(color,curveInterval); + boolean isCurveCompletelyDrawn; + if (!Options.isExperimentalSliderStyle()) { + isCurveCompletelyDrawn = Options.isSliderSnaking() || alpha == 1f; + curve.draw(color, isCurveCompletelyDrawn ? 1f : alpha); + } else { + curveColor.a = sliderAlpha; + isCurveCompletelyDrawn = drawSliderTrack(trackPosition, Utils.clamp(1d - (double) (timeDiff - approachTime + fadeInTime) / fadeInTime, 0d, 1d)); + color.a = alpha; + } // end circle (only draw if ball still has to go there) - if (curveInterval == 1f && currentRepeats < repeatCount - (repeatCount % 2 == 0 ? 1 : 0)) { + if (isCurveCompletelyDrawn && (!Options.isExperimentalSliderStyle() || Options.isDrawSliderEndCircles()) && currentRepeats < repeatCount - (repeatCount % 2 == 0 ? 1 : 0)) { Color circleColor = new Color(color); Color overlayColor = new Color(Colors.WHITE_FADE); if (currentRepeats == 0) { @@ -228,7 +241,7 @@ public void draw(Graphics g, int trackPosition) { // fade in end circle after repeats circleColor.a = overlayColor.a = sliderAlpha * getCircleAlphaAfterRepeat(trackPosition, true); } - Vec2f endCircPos = curve.pointAt(curveInterval); + Vec2f endCircPos = curve.pointAt(1f); hitCircle.drawCentered(endCircPos.x, endCircPos.y, circleColor); hitCircleOverlay.drawCentered(endCircPos.x, endCircPos.y, overlayColor); } @@ -277,13 +290,15 @@ public void draw(Graphics g, int trackPosition) { } // repeats - if (curveInterval == 1.0f) { + if (isCurveCompletelyDrawn) { for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1 && tcurRepeat < repeatCount - 1; tcurRepeat++) { Image arrow = GameImage.REVERSEARROW.getImage(); // bouncing animation //arrow = arrow.getScaledCopy((float) (1 + 0.2d * ((trackPosition + sliderTime * tcurRepeat) % 292) / 292)); - float colorLuminance = Utils.getLuminance(color); - Color arrowColor = colorLuminance < 0.8f ? Color.white : Color.black; + Color arrowColor = Color.white; + if (!Options.isExperimentalSliderStyle() && Utils.getLuminance(color) >= 0.8f) { + arrowColor = Color.black; + } if (tcurRepeat == 0) { arrow.setAlpha(Options.isSliderSnaking() ? decorationsAlpha : 1f); } else { @@ -406,6 +421,42 @@ private void drawSliderTicks(int trackPosition, float curveAlpha, float decorati } } + /** + * Draws the slider track for the experimental style sliders. + * @param trackPosition position of the song + * @param snakingSliderProgress progress of the snaking sliders [0, 1] + * @return true if the track was completely drawn + */ + private boolean drawSliderTrack(int trackPosition, double snakingSliderProgress) { + double curveIntervalTo = Options.isSliderSnaking() ? snakingSliderProgress : 1d; + double curveIntervalFrom = 0d; + if (Options.isShrinkingSliders()) { + double sliderprogress = (trackPosition - hitObject.getTime() - ((double) sliderTime * (hitObject.getRepeatCount() - 1))) / (double) sliderTime; + if (sliderprogress > 0) { + curveIntervalFrom = sliderprogress; + } + } + int curvelen = curve.getCurvePoints().length; + if (Options.isMergingSliders()) { + if (Options.isShrinkingSliders() && curveIntervalFrom > 0) { + if (hitObject.getRepeatCount() % 2 == 0) { + game.addMergedSliderPointsToRender(baseSliderFrom, baseSliderFrom + (int) ((1d - curveIntervalFrom) * curvelen)); + } else { + game.addMergedSliderPointsToRender(baseSliderFrom + (int) (curveIntervalFrom * curvelen) + 1, baseSliderFrom + (int) (curveIntervalTo * curve.getCurvePoints().length)); + } + } else { + game.addMergedSliderPointsToRender(baseSliderFrom, baseSliderFrom + (int) (curveIntervalTo * curve.getCurvePoints().length)); + } + } else { + if (Options.isShrinkingSliders() && curveIntervalFrom > 0 && hitObject.getRepeatCount() % 2 == 0) { + curve.splice((int) ((1d - curveIntervalFrom) * curvelen), curvelen); + curveIntervalFrom = 0d; + } + curve.draw(curveColor, (int) (curveIntervalFrom * curvelen), (int) (curveIntervalTo * curvelen)); + } + return curveIntervalTo == 1d; + } + /** * Get the alpha level used to fade in circles & reversearrows after repeat * @param trackPosition current trackposition, in ms @@ -707,7 +758,6 @@ else if (GameMod.RELAX.isActive() && trackPosition >= time) // calculate and send slider result hitResult(); - return true; } @@ -753,6 +803,10 @@ private float getT(int trackPosition, boolean raw) { } } + public Curve getCurve() { + return curve; + } + @Override public void reset() { sliderClickedInitial = false; diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index e4fd34c4..0afb7bd6 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -23,6 +23,7 @@ import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.options.Options; import itdelatrisu.opsu.render.CurveRenderState; +import itdelatrisu.opsu.render.LegacyCurveRenderState; import itdelatrisu.opsu.skins.Skin; import itdelatrisu.opsu.ui.Colors; @@ -42,7 +43,7 @@ public abstract class Curve { protected static float CURVE_POINTS_SEPERATION = 5; /** The curve border color. */ - private static Color borderColor; + protected static Color borderColor; /** Whether mmsliders are supported. */ private static boolean mmsliderSupported = false; @@ -59,9 +60,20 @@ public abstract class Curve { /** Per-curve render-state used for the new style curve renders. */ private CurveRenderState renderState; + /** Per-curve render-state used for the legacy style curve renders. */ + protected LegacyCurveRenderState legacyRenderState; + /** Points along the curve (set by inherited classes). */ protected Vec2f[] curve; + /** + * Get the curve points + * @return curve points + */ + public Vec2f[] getCurvePoints() { + return curve; + } + /** * Constructor. * @param hitObject the associated HitObject @@ -96,9 +108,10 @@ public static void init(int width, int height, float circleDiameter, Color borde ContextCapabilities capabilities = GLContext.getCapabilities(); mmsliderSupported = capabilities.OpenGL30; - if (mmsliderSupported) + if (mmsliderSupported) { CurveRenderState.init(width, height, circleDiameter); - else { + LegacyCurveRenderState.init(width, height, circleDiameter); + } else { if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER) Log.warn("New slider style requires OpenGL 3.0."); } @@ -147,6 +160,25 @@ public void draw(Color color, float t) { } } + public void draw(Color color, int from, int to) { + if (curve == null) + return; + if (legacyRenderState == null) + legacyRenderState = new LegacyCurveRenderState(hitObject, curve); + legacyRenderState.draw(color, borderColor, from, to); + } + + /** + * Splice the curve to draw (= flag a part of it as 'do not draw'). Bases on the curve point indices. + * @param from start index to splice + * @param to end index to splice + */ + public void splice(int from, int to) { + if (legacyRenderState == null) + return; + legacyRenderState.splice(from, to); + } + /** * Returns the angle of the first control point. */ @@ -175,5 +207,7 @@ public void draw(Color color, float t) { public void discardGeometry() { if (renderState != null) renderState.discardGeometry(); + if (legacyRenderState != null) + legacyRenderState.discardGeometry(); } } diff --git a/src/itdelatrisu/opsu/objects/curves/FakeCombinedCurve.java b/src/itdelatrisu/opsu/objects/curves/FakeCombinedCurve.java new file mode 100644 index 00000000..9050de23 --- /dev/null +++ b/src/itdelatrisu/opsu/objects/curves/FakeCombinedCurve.java @@ -0,0 +1,68 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014-2017 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.objects.curves; + +import itdelatrisu.opsu.beatmap.HitObject; +import itdelatrisu.opsu.render.LegacyCurveRenderState; +import org.newdawn.slick.Color; + +import java.util.ArrayList; +import java.util.List; + +public class FakeCombinedCurve extends Curve { + + private List pointsToRender; + + public FakeCombinedCurve(Vec2f[] points) { + super(new HitObject(0, 0, 0), false); + this.curve = points; + pointsToRender = new ArrayList<>(); + } + + public void initForFrame() { + pointsToRender.clear(); + } + + public void addRange(int from, int to) { + pointsToRender.add(from); + pointsToRender.add(to); + } + + @Override + public void draw(Color color) { + if (legacyRenderState == null) + legacyRenderState = new LegacyCurveRenderState(hitObject, curve); + legacyRenderState.draw(color, borderColor, pointsToRender); + } + + @Override + public Vec2f pointAt(float t) { + return null; + } + + @Override + public float getEndAngle() { + return 0; + } + + @Override + public float getStartAngle() { + return 0; + } + +} diff --git a/src/itdelatrisu/opsu/options/OptionGroup.java b/src/itdelatrisu/opsu/options/OptionGroup.java index 07a3e244..cc5f8a95 100644 --- a/src/itdelatrisu/opsu/options/OptionGroup.java +++ b/src/itdelatrisu/opsu/options/OptionGroup.java @@ -51,6 +51,12 @@ public class OptionGroup { GameOption.SHOW_FOLLOW_POINTS, GameOption.SCREENSHOT_FORMAT, }), + new OptionGroup("EXPERIMENTAL SLIDERS", new GameOption[] { + GameOption.EXPERIMENTAL_SLIDERS, + GameOption.EXPERIMENTAL_SLIDERS_MERGE, + GameOption.EXPERIMENTAL_SLIDERS_SHRINK, + GameOption.EXPERIMENTAL_SLIDERS_DRAW_ENDCIRCLES, + }), new OptionGroup("MAIN MENU", new GameOption[] { GameOption.DYNAMIC_BACKGROUND, GameOption.PARALLAX, diff --git a/src/itdelatrisu/opsu/options/Options.java b/src/itdelatrisu/opsu/options/Options.java index f907b5f6..8a1e913f 100644 --- a/src/itdelatrisu/opsu/options/Options.java +++ b/src/itdelatrisu/opsu/options/Options.java @@ -567,6 +567,10 @@ public void setValue(int value) { IGNORE_BEATMAP_SKINS ("Ignore all beatmap skins", "IgnoreBeatmapSkins", "Defaults game settings to never use skin element overrides provided by beatmaps.", false), FORCE_SKIN_CURSOR ("Always use skin cursor", "UseSkinCursor", "The selected skin's cursor will override any beatmap-specific cursor modifications.", false), SNAKING_SLIDERS ("Snaking sliders", "SnakingSliders", "Sliders gradually snake out from their starting point.", true), + EXPERIMENTAL_SLIDERS ("Enable", "ExperimentalSliders", "Enable experimental slider style", false), + EXPERIMENTAL_SLIDERS_DRAW_ENDCIRCLES ("Draw slider endcircles", "ExSlDrawEnd", "Only for experimental sliders", false), + EXPERIMENTAL_SLIDERS_SHRINK ("Shrinking sliders", "ExSlShrink", "Sliders shrink toward their ending point when the ball passes.", true), + EXPERIMENTAL_SLIDERS_MERGE ("Merging sliders", "ExSlMerge", "Don't draw edges and combine slider tracks where sliders overlap.", true), SHOW_HIT_LIGHTING ("Hit lighting", "HitLighting", "Adds a subtle glow behind hit explosions which lights the playfield.", true), SHOW_COMBO_BURSTS ("Combo bursts", "ComboBurst", "A character image bursts from the side of the screen at combo milestones.", true), SHOW_PERFECT_HIT ("Perfect hits", "PerfectHit", "Shows perfect hit result bursts (300s, slider ticks).", true), @@ -1176,6 +1180,11 @@ public static void setDisplayMode(Container app) { */ public static boolean isSliderSnaking() { return GameOption.SNAKING_SLIDERS.getBooleanValue(); } + public static boolean isExperimentalSliderStyle() { return GameOption.EXPERIMENTAL_SLIDERS.getBooleanValue(); } + public static boolean isDrawSliderEndCircles() { return GameOption.EXPERIMENTAL_SLIDERS_DRAW_ENDCIRCLES.getBooleanValue(); } + public static boolean isShrinkingSliders() { return GameOption.EXPERIMENTAL_SLIDERS_SHRINK.getBooleanValue(); } + public static boolean isMergingSliders() { return GameOption.EXPERIMENTAL_SLIDERS_MERGE.getBooleanValue(); } + /** * Returns the fixed circle size override, if any. * @return the CS value (0, 10], 0f if disabled diff --git a/src/itdelatrisu/opsu/render/LegacyCurveRenderState.java b/src/itdelatrisu/opsu/render/LegacyCurveRenderState.java new file mode 100644 index 00000000..45bd0232 --- /dev/null +++ b/src/itdelatrisu/opsu/render/LegacyCurveRenderState.java @@ -0,0 +1,595 @@ +/* + * 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.Utils; +import itdelatrisu.opsu.beatmap.HitObject; +import itdelatrisu.opsu.objects.Circle; +import itdelatrisu.opsu.objects.curves.Vec2f; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.*; +import org.newdawn.slick.Color; +import org.newdawn.slick.Image; +import org.newdawn.slick.util.Log; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.Iterator; +import java.util.List; + +/** + * Hold the temporary render state that needs to be restored again after the new + * style curves are drawn. + * + * @author Bigpet {@literal } + */ +public class LegacyCurveRenderState { + /** The width and height of the display container this curve gets drawn into. */ + protected static int containerWidth, containerHeight; + + /** Thickness of the curve. */ + protected static int scale; + + /** Static state that's needed to draw the new style curves. */ + private static final NewCurveStyleState staticState = new NewCurveStyleState(); + + /** Cached drawn slider, only used if new style sliders are activated. */ + public Rendertarget fbo; + + /** The HitObject associated with the curve to be drawn. */ + protected HitObject hitObject; + + 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; + + /** The point from which the curve has last been rendered into the texture (as an index into {@code curve}). */ + private int firstPointDrawn; + + /** Index of the curve array that indicates the starting position of the slider part that should not be drawn. */ + private int spliceFrom; + + /** Index of the curve array that indicates the end position of the slider part that should not be drawn. */ + private int spliceTo; + + /** List holding pairs of 2 numbers in a sequential order, indicating the start- and endindex of the curvearray to render. */ + protected List pointsToRender; + + /** + * Set the width and height of the container that Curves get drawn into. + * Should be called before any curves are drawn. + * @param width the container width + * @param height the container height + * @param circleDiameter the circle diameter + */ + public static void init(int width, int height, float circleDiameter) { + containerWidth = width; + containerHeight = height; + + // equivalent to what happens in Slider.init() + 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, int, int)} + * are undone. + */ + public static void shutdown() { + staticState.shutdown(); + FrameBufferCache.shutdown(); + } + + /** + * 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 LegacyCurveRenderState(HitObject hitObject, Vec2f[] curve) { + this.hitObject = hitObject; + this.curve = curve; + initFBO(); + } + + + private void initFBO() { + FrameBufferCache cache = FrameBufferCache.getInstance(); + Rendertarget mapping = cache.get(hitObject); + if(mapping == null) + mapping = cache.insert(hitObject); + fbo = mapping; + + createVertexBuffer(fbo.getVbo() + + ); + //write impossible value to make sure the fbo is cleared + lastPointDrawn=-1; + spliceFrom=spliceTo=-1; + } + + /** + * Splice the curve + * @param from start index to splice + * @param to end index to splice + */ + public void splice(int from, int to) { + spliceFrom = from * 2; + spliceTo = to * 2; + firstPointDrawn = -1; // force redraw + lastPointDrawn = -1; // force redraw + } + + public void draw(Color color, Color borderColor, List pointsToRender) { + lastPointDrawn = -1; + firstPointDrawn = -1; + this.pointsToRender = pointsToRender; + draw(color, borderColor, 0, curve.length); + this.pointsToRender = null; + } + + /** + * Draw a curve to the screen that's tinted with `color`. The first time + * this is called this caches the image result of the curve and on subsequent + * runs it just draws the cached copy to the screen. + * @param color tint of the curve + * @param borderColor the curve border color + * @param from index to draw from + * @param to index to draw to (exclusive) + */ + public void draw(Color color, Color borderColor, int from, int to) { + float alpha = color.a; + + if (fbo == null) { + // this should not be null, but issue #106 claims it is possible, at least 3 people had this... + // debugging shows that the draw was called after discardGeometry was, which does not really make sense + initFBO(); + } + + if (lastPointDrawn != to || firstPointDrawn != from) { + 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); + this.renderCurve(color, borderColor, from, to, firstPointDrawn != from); + lastPointDrawn = to; + firstPointDrawn = from; + 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)); + } + + // 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(); + } + + /** + * Discard the cache mapping for this curve object. + */ + public void discardGeometry() { + fbo = null; + FrameBufferCache.getInstance().freeMappingFor(hitObject); + } + + /** + * A structure to hold all the important OpenGL state that needs to be + * changed to draw the curve. This is used to backup and restore the state + * so that the code outside of this (mainly Slick2D) doesn't break. + */ + private class RenderState { + boolean smoothedPoly; + boolean blendEnabled; + boolean depthEnabled; + boolean depthWriteEnabled; + boolean texEnabled; + int texUnit; + int oldProgram; + int oldArrayBuffer; + } + + /** + * Backup the current state of the relevant OpenGL state and change it to + * what's needed to draw the curve. + */ + private RenderState saveRenderState() { + 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.glDisable(GL11.GL_BLEND); + 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; + } + + /** + * Restore the old OpenGL state that's backed up in {@code state}. + * @param state the old state to restore + */ + private void restoreRenderState(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); + } + + /** + * 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 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(); + if (curve.length > 0) { + fillCone(buff, curve[0].x, curve[0].y); + } + + for (int i = 1; i < curve.length; ++i) { + float x = curve[i].x; + float y = curve[i].y; + fillCone(buff, x, y); + 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; + float dist = Utils.distance(x, y, last_x, last_y); + if (dist < Circle.diameter / 8) { + x = (float) (x - diff_x / 2); + y = (float) (y - diff_y / 2); + } else { + // don't mind me + x = -100f; + y = -100f; + } + fillCone(buff, x, y); + } + buff.flip(); + 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, boolean clearFirst) { + 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); + GL20.glUniform1i(staticState.texLoc, 0); + GL20.glUniform3f(staticState.colLoc, color.r, color.g, color.b); + GL20.glUniform4f(staticState.colBorderLoc, borderColor.r, borderColor.g, borderColor.b, borderColor.a); + //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(staticState.attribLoc, 4, GL11.GL_FLOAT, false, 6 * 4, 2 * 4); + GL20.glVertexAttribPointer(staticState.texCoordLoc, 2, GL11.GL_FLOAT, false, 6 * 4, 0); + if (clearFirst) { + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + } + if (pointsToRender == null) { + renderCurve(from, to); + } else { + renderCurve(); + } + GL11.glFlush(); + GL20.glDisableVertexAttribArray(staticState.texCoordLoc); + GL20.glDisableVertexAttribArray(staticState.attribLoc); + restoreRenderState(state); + } + + private void renderCurve(int from, int to) { + for (int i = from * 2; i < to * 2 - 1; ++i) { + if (spliceFrom <= i && i <= spliceTo) { + continue; + } + GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2); + } + } + + private void renderCurve() { + Iterator iter = pointsToRender.iterator(); + while (iter.hasNext()) { + for (int i = iter.next() * 2, end = iter.next() * 2 - 1; i < end; ++i) { + GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2); + } + } + } + + /** + * Fill {@code buff} with the texture coordinates and positions for a cone + * 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 + */ + 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 radius = scale / 2; + + for (int i = 0; i < NewCurveStyleState.unitCone.length / 6; ++i) { + buff.put(NewCurveStyleState.unitCone[i * 6 + 0]); + buff.put(NewCurveStyleState.unitCone[i * 6 + 1]); + buff.put(offx + (x1 + radius * NewCurveStyleState.unitCone[i * 6 + 2]) / divx); + buff.put(offy - (y1 + radius * NewCurveStyleState.unitCone[i * 6 + 3]) / divy); + buff.put(NewCurveStyleState.unitCone[i * 6 + 4]); + buff.put(NewCurveStyleState.unitCone[i * 6 + 5]); + } + } + + /** + * 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; + + /** + * Array to hold the dummy vertex data (texture coordinates and position) + * of a cone with DIVIDES vertices at its base, that is centered around + * (0,0) and has a radius of 1 (so that it can be translated and scaled easily). + */ + protected static float[] unitCone = new float[(DIVIDES + 2) * 6]; + + /** 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 shader uniform location of the border color attribute. */ + protected int colBorderLoc = 0; + + /** OpenGL shader uniform location of the texture sampler attribute. */ + protected int texLoc = 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_EX.getImage().getScaledCopy(1.0f / GameImage.getUIscale()); + 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); + EXTFramebufferObject.glGenerateMipmapEXT(GL11.GL_TEXTURE_1D); + } + } + + /** + * Write the data into {@code unitCone} if it hasn't already been initialized. + */ + public static void initUnitCone() { + int index = 0; + //check if initialization has already happened + if (unitCone[0] == 0.0f) { + //tip of the cone + //vec2 texture coordinates + unitCone[index++] = 1.0f; + unitCone[index++] = 0.5f; + + //vec4 position + unitCone[index++] = 0.0f; + unitCone[index++] = 0.0f; + unitCone[index++] = 0.0f; + unitCone[index++] = 1.0f; + for (int j = 0; j < NewCurveStyleState.DIVIDES; ++j) { + double phase = j * (float) Math.PI * 2 / NewCurveStyleState.DIVIDES; + //vec2 texture coordinates + unitCone[index++] = 0.0f; + unitCone[index++] = 0.5f; + //vec4 positon + unitCone[index++] = (float) Math.sin(phase); + unitCone[index++] = (float) Math.cos(phase); + unitCone[index++] = 1.0f; + unitCone[index++] = 1.0f; + } + //vec2 texture coordinates + unitCone[index++] = 0.0f; + unitCone[index++] = 0.5f; + //vec4 positon + unitCone[index++] = (float) Math.sin(0.0f); + unitCone[index++] = (float) Math.cos(0.0f); + unitCone[index++] = 1.0f; + unitCone[index++] = 1.0f; + } + } + + /** + * 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 110\n" + + "\n" + + "attribute vec4 in_position;\n" + + "attribute vec2 in_tex_coord;\n" + + "\n" + + "varying 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 110\n" + + "\n" + + "uniform sampler1D tex;\n" + + "uniform vec2 tex_size;\n" + + "uniform vec3 col_tint;\n" + + "uniform vec4 col_border;\n" + + "\n" + + "varying vec2 tex_coord;\n" + + "\n" + + "void main()\n" + + "{\n" + + " vec4 in_color = texture1D(tex, tex_coord.x);\n" + + " float blend_factor = in_color.r-in_color.b;\n" + + " vec4 new_color = vec4(mix(in_color.xyz*col_border.xyz,col_tint,blend_factor),in_color.w);\n" + + " gl_FragColor = 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)); + } + GL20.glDeleteShader(vtxShdr); + GL20.glDeleteShader(frgShdr); + attribLoc = GL20.glGetAttribLocation(program, "in_position"); + texCoordLoc = GL20.glGetAttribLocation(program, "in_tex_coord"); + texLoc = GL20.glGetUniformLocation(program, "tex"); + colLoc = GL20.glGetUniformLocation(program, "col_tint"); + colBorderLoc = GL20.glGetUniformLocation(program, "col_border"); + } + } + + /** + * Cleanup any OpenGL objects that may have been initialized. + */ + private void shutdown() { + if (gradientTexture != 0) { + GL11.glDeleteTextures(gradientTexture); + gradientTexture = 0; + } + + if (program != 0) { + GL20.glDeleteProgram(program); + program = 0; + attribLoc = 0; + texCoordLoc = 0; + colLoc = 0; + colBorderLoc = 0; + texLoc = 0; + } + } + } +} diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 83220e61..811f3db0 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -42,6 +42,7 @@ import itdelatrisu.opsu.objects.Slider; import itdelatrisu.opsu.objects.Spinner; import itdelatrisu.opsu.objects.curves.Curve; +import itdelatrisu.opsu.objects.curves.FakeCombinedCurve; import itdelatrisu.opsu.objects.curves.Vec2f; import itdelatrisu.opsu.options.Options; import itdelatrisu.opsu.render.FrameBufferCache; @@ -63,11 +64,7 @@ import java.io.File; import java.io.IOException; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Stack; +import java.util.*; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.Display; @@ -319,6 +316,9 @@ public enum Restart { /** The video seek time (if any). */ private int videoSeekTime; + /** The merged slider. */ + private FakeCombinedCurve mergedslider; + /** Music position bar background colors. */ private static final Color MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f), @@ -1201,6 +1201,7 @@ else if (key == Options.getGameKeyRight()) beatmap.objects[objectIndex++].getTime() <= checkpoint) ; objectIndex--; + lastReplayTime = beatmap.objects[objectIndex].getTime(); lastTrackPosition = checkpoint; } catch (SlickException e) { @@ -1586,6 +1587,32 @@ else if (hitObject.isSpinner()) MusicController.pause(); SoundController.mute(false); + + if (Options.isMergingSliders()) { + if (!Options.isShrinkingSliders()) { + mergedslider = null; // workaround for yugecin/opsu-dance#130 + } + if (mergedslider == null) { + List curvepoints = new ArrayList<>(); + for (GameObject gameObject : gameObjects) { + if (gameObject instanceof Slider) { + ((Slider) gameObject).baseSliderFrom = curvepoints.size(); + curvepoints.addAll(Arrays.asList(((Slider) gameObject).getCurve().getCurvePoints())); + } + } + if (curvepoints.size() > 0) { + mergedslider = new FakeCombinedCurve(curvepoints.toArray(new Vec2f[curvepoints.size()])); + } + } else { + int base = 0; + for (GameObject gameObject : gameObjects) { + if (gameObject instanceof Slider) { + ((Slider) gameObject).baseSliderFrom = base; + base += ((Slider) gameObject).getCurve().getCurvePoints().length; + } + } + } + } } skipButton.resetHover(); @@ -1603,11 +1630,17 @@ public void leave(GameContainer container, StateBasedGame game) if (GameMod.AUTO.isActive() || isReplay) UI.getCursor().hide(); + mergedslider = null; + // replays if (isReplay) GameMod.loadModState(previousMods); } + public void addMergedSliderPointsToRender(int from, int to) { + mergedslider.addRange(from, to); + } + /** * Draws hit objects, hit results, and follow points. * @param g the graphics context @@ -1617,6 +1650,11 @@ private void drawHitObjects(Graphics g, int trackPosition) { // draw result objects (under) data.drawHitResults(trackPosition, false); + if (Options.isMergingSliders() && mergedslider != null) { + mergedslider.draw(Color.white); + mergedslider.initForFrame(); + } + // include previous object in follow points int lastObjectIndex = -1; if (objectIndex > 0 && objectIndex < beatmap.objects.length &&