Merge pull request #134 from Bigpet/snakeslider

Snaking sliders implementation.
This commit is contained in:
Jeffrey Han 2015-09-18 15:27:54 -04:00
commit daf3a2aad3
7 changed files with 203 additions and 104 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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,16 +235,23 @@ public class Slider implements GameObject {
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
// repeats
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)
if (sliderTime == 0) {
continue;
}
float t = Math.max(getT(trackPosition, true), 0);
arrow.setAlpha((float) (t - Math.floor(t)));
} else
} else {
if(Options.isSliderSnaking()){
arrow.setAlpha(decorationsAlpha);
} else {
arrow.setAlpha(1f);
}
}
if (tcurRepeat % 2 == 0) {
// last circle
arrow.setRotation(curve.getEndAngle());
@ -245,6 +263,7 @@ public class Slider implements GameObject {
}
}
}
}
if (timeDiff >= 0) {
// approach circle

View File

@ -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);
}
}

View File

@ -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);
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);
Colors.WHITE_FADE.a = 1.0f;
this.draw_curve(color, borderColor, curve);
}
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<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]);
}
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);
}
/**
@ -354,6 +359,13 @@ public class CurveRenderState {
*/
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;
@ -398,6 +410,47 @@ public class CurveRenderState {
}
}
/**
* 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.

View File

@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
import org.lwjgl.opengl.EXTFramebufferObject;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
/**
* Represents a rendertarget. For now this maps to an OpenGL FBO via LWJGL.
@ -31,6 +32,9 @@ public class Rendertarget {
/** The dimensions. */
public final int width, height;
/** ID of the vertex buffer associated with this rendertarget*/
private final int vboID;
/** The FBO ID. */
private final int fboID;
@ -49,6 +53,7 @@ public class Rendertarget {
this.width = width;
this.height = height;
fboID = EXTFramebufferObject.glGenFramebuffersEXT();
vboID = GL15.glGenBuffers();
textureID = GL11.glGenTextures();
depthBufferID = EXTFramebufferObject.glGenRenderbuffersEXT();
}
@ -60,19 +65,27 @@ public class Rendertarget {
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fboID);
}
/**
* Returns the FBO ID.
* Get the ID of the VBO associated with this Rendertarget.
* @return OpenGL buffer ID for the VBO
*/
public int getVbo() {
return vboID;
}
/**
* Get the FBO ID.
* @return the OpenGL FBO ID
*/
// NOTE: 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;
}
/**
* Returns the texture ID.
* Get the texture ID of the texture this rendertarget renders into.
* @return the OpenGL texture ID
*/
// NOTE: try not to use, could be moved into separate class.
public int getTextureID() {
return textureID;
}
@ -89,6 +102,7 @@ public class Rendertarget {
* and a renderbuffer that it renders the depth to.
* @param width the width
* @param height the height
* @return the newly created Rendertarget instance
*/
public static Rendertarget createRTTFramebuffer(int width, int height) {
int old_framebuffer = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
@ -122,5 +136,6 @@ public class Rendertarget {
EXTFramebufferObject.glDeleteFramebuffersEXT(fboID);
EXTFramebufferObject.glDeleteRenderbuffersEXT(depthBufferID);
GL11.glDeleteTextures(textureID);
GL15.glDeleteBuffers(vboID);
}
}

View File

@ -76,6 +76,7 @@ public class OptionsMenu extends BasicGameState {
GameOption.BACKGROUND_DIM,
GameOption.FORCE_DEFAULT_PLAYFIELD,
GameOption.IGNORE_BEATMAP_SKINS,
GameOption.SNAKING_SLIDERS,
GameOption.SHOW_HIT_LIGHTING,
GameOption.SHOW_COMBO_BURSTS,
GameOption.SHOW_PERFECT_HIT,