New optional slider style

New slider rendering works by rendering the slider to an
offscreen buffer

Add CurveRenderState.java and FrameBufferCache.java that were forgotten in the last commit
This commit is contained in:
Peter Tissen 2015-03-30 14:19:39 +02:00
parent 90c8c9e705
commit 41c7825728
11 changed files with 790 additions and 28 deletions

BIN
res/slidergradient.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -374,6 +374,18 @@ public class GameData {
health = 100f;
healthDisplay = 100f;
hitResultCount = new int[HIT_MAX];
if(hitResultList != null)
{
Iterator<OsuHitObjectResult> iter =hitResultList.iterator();
while(iter.hasNext())
{
OsuHitObjectResult hitObj = iter.next();
if(hitObj != null && hitObj.curve != null)
{
hitObj.curve.discardCache();
}
}
}
hitResultList = new LinkedBlockingDeque<OsuHitObjectResult>();
hitErrorList = new LinkedBlockingDeque<HitErrorInfo>();
fullObjectCount = 0;
@ -939,10 +951,15 @@ public class GameData {
}
hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME);
} else
} else{
if (hitResult.curve !=null)
{
hitResult.curve.discardCache();
}
iter.remove();
}
}
}
/**
* Changes health by a given percentage, modified by drainRate.

View File

@ -110,6 +110,7 @@ public enum GameImage {
APPROACHCIRCLE ("approachcircle", "png"),
// Slider
SLIDER_GRADIENT ("slidergradient", "png"),
SLIDER_BALL ("sliderb", "sliderb%d", "png"),
SLIDER_FOLLOWCIRCLE ("sliderfollowcircle", "png"),
REVERSEARROW ("reversearrow", "png"),

View File

@ -228,6 +228,7 @@ public class Options {
UI.getCursor().reset();
}
},
NEW_SLIDER("Enable New Slider", "Use the new Slider style (requires OpenGL 3.0).",false),
DYNAMIC_BACKGROUND ("Enable Dynamic Backgrounds", "The song background will be used as the main menu background.", true),
BACKGROUND_DIM ("Background Dim", "Percentage to dim the background image during gameplay.", 50, 0, 100),
FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "Override the song background with the default playfield background.", false),
@ -484,6 +485,9 @@ public class Options {
/** The current skin. */
private static Skin skin;
/** Resolution that the latest Container passed to "setDisplayMode" was set to. */
private static Resolution lastSetResolution = Resolution.RES_1024_768;
/** Frame limiters. */
private static final int[] targetFPS = { 60, 120, 240 };
@ -593,6 +597,8 @@ public class Options {
// set borderless window if dimensions match screen size
boolean borderless = (screenWidth == resolution.getWidth() && screenHeight == resolution.getHeight());
System.setProperty("org.lwjgl.opengl.Window.undecorated", Boolean.toString(borderless));
lastSetResolution = resolution;
}
// /**
@ -887,6 +893,24 @@ public class Options {
return replayDir;
}
/**
* Returns the horizontal Resolution.
* If no game has been started then a default value is returned.
* @return the horizontal resolution of the latest game instance to be started.
*/
public static int getLatestResolutionWidth() {
return lastSetResolution.getWidth();
}
/**
* Returns the vertical Resolution.
* If no game has been started then a default value is returned.
* @return the vertical resolution of the latest game instance to be started.
*/
public static int getLatestResolutionHeight() {
return lastSetResolution.getHeight();
}
/**
* Returns the current skin directory.
* If invalid, this will create a "Skins" folder in the root directory.
@ -1071,6 +1095,9 @@ public class Options {
case "NewCursor":
GameOption.NEW_CURSOR.setValue(Boolean.parseBoolean(value));
break;
case "NewSlider":
GameOption.NEW_SLIDER.setValue(Boolean.parseBoolean(value));
break;
case "DynamicBackground":
GameOption.DYNAMIC_BACKGROUND.setValue(Boolean.parseBoolean(value));
break;
@ -1222,6 +1249,8 @@ public class Options {
writer.newLine();
writer.write(String.format("NewCursor = %b", isNewCursorEnabled()));
writer.newLine();
writer.write(String.format("NewSlider = %b", GameOption.NEW_SLIDER.getBooleanValue()));
writer.newLine();
writer.write(String.format("DynamicBackground = %b", isDynamicBackgroundEnabled()));
writer.newLine();
writer.write(String.format("LoadVerbose = %b", isLoadVerbose()));

View File

@ -30,12 +30,20 @@ import itdelatrisu.opsu.objects.curves.CatmullCurve;
import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.LinearBezier;
import itdelatrisu.opsu.render.Rendertarget;
import itdelatrisu.opsu.states.Game;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL14;
import org.lwjgl.opengl.GL30;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.util.Log;
/**
* Data type representing a slider object.
@ -50,6 +58,9 @@ public class Slider implements GameObject {
/** Rate at which slider ticks are placed. */
private static float sliderTickRate = 1.0f;
/** Scaling factor for display elements */
private static int diameter = 1;
/** The amount of time, in milliseconds, to fade in the slider. */
private static final int FADE_IN_TIME = 375;
@ -111,7 +122,7 @@ public class Slider implements GameObject {
containerWidth = container.getWidth();
containerHeight = container.getHeight();
int diameter = (int) (104 - (circleSize * 8));
diameter = (int) (104 - (circleSize * 8));
diameter = (int) (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
// slider ball
@ -170,12 +181,26 @@ public class Slider implements GameObject {
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
float approachScale = 1 + scale * 3;
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
Image hitCircle = GameImage.HITCIRCLE.getImage();
float[] endPos = curve.pointAt(1);
// curve
curve.draw(color);
color.a = alpha;
// end circle
hitCircle.drawCentered(endPos[0], endPos[1], color);
hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
// start circle
hitCircle.drawCentered(x, y, color);
if (!overlayAboveNumber) {
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
}
// ticks
if (ticksT != null) {
@ -185,20 +210,6 @@ public class Slider implements GameObject {
tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE);
}
}
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
Image hitCircle = GameImage.HITCIRCLE.getImage();
// end circle
float[] endPos = curve.pointAt(1);
hitCircle.drawCentered(endPos[0], endPos[1], color);
hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
// start circle
hitCircle.drawCentered(x, y, color);
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
if (!overlayAboveNumber)
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
if (sliderClickedInitial)
; // don't draw current combo number if already clicked
else
@ -388,6 +399,7 @@ public class Slider implements GameObject {
// calculate and send slider result
hitResult();
return true;
}
@ -469,6 +481,8 @@ public class Slider implements GameObject {
this.curve = new CatmullCurve(hitObject, color);
else
this.curve = new LinearBezier(hitObject, color, hitObject.getSliderType() == HitObject.SLIDER_LINEAR);
this.curve.setScale(diameter );//* 118 / 128);
}
@Override

View File

@ -18,12 +18,15 @@
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.render.CurveRenderState;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.Options;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
import org.newdawn.slick.util.Log;
/**
* Representation of a curve.
@ -43,6 +46,12 @@ public abstract class Curve {
/** The scaled slider x, y coordinate lists. */
protected float[] sliderX, sliderY;
/** scaling factor for drawing. */
protected static float scale;
/** Per-curve render-state used for the new style curve renders*/
private CurveRenderState renderState;
/** Points along the curve (set by inherited classes). */
protected Vec2f[] curve;
@ -57,6 +66,8 @@ public abstract class Curve {
this.y = hitObject.getScaledY();
this.sliderX = hitObject.getScaledSliderX();
this.sliderY = hitObject.getScaledSliderY();
this.scale = 100;
this.renderState = null;
}
/**
@ -71,16 +82,27 @@ public abstract class Curve {
* @param color the color filter
*/
public void draw(Color color) {
if (curve == null)
if ( curve == null){
Log.error("draw curve"+this);
return;
}
if (Options.GameOption.NEW_SLIDER.getBooleanValue()) {
if(renderState == null)
{
renderState = new CurveRenderState(scale,hitObject);
}
renderState.draw(color,curve);
} else {
Image hitCircle = GameImage.HITCIRCLE.getImage();
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
for (int i = 0; i < curve.length; i++)
for (int i = 0; i < curve.length; i++) {
hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE);
for (int i = 0; i < curve.length; i++)
}
for (int i = 0; i < curve.length; i++){
hitCircle.drawCentered(curve[i].x, curve[i].y, color);
}
}
}
/**
* Returns the angle of the first control point.
@ -104,10 +126,23 @@ public abstract class Curve {
*/
public float getY(int i) { return (i == 0) ? y : sliderY[i - 1]; }
/**
* Set the scaling factor.
* @param factor the new scaling factor for the UI representation
*/
public static void setScale(float factor) {
scale = factor;
}
/**
* Linear interpolation of a and b at t.
*/
protected float lerp(float a, float b, float t) {
return a * (1 - t) + b * t;
}
public void discardCache() {
if(renderState != null)
renderState.discardCache();
}
}

View File

@ -0,0 +1,428 @@
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.render;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL14;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
import org.newdawn.slick.util.Log;
/**
* Hold the temporary render state that needs to be restored again after the new
* style curves are drawn.
*/
public class CurveRenderState {
/** cached drawn slider, only used if new style sliders are activated */
public Rendertarget fbo;
/** thickness of the curve */
float scale;
/** The HitObject associated with the curve to be drawn */
HitObject hitObject;
/** Static state that's needed to draw the new style curves */
private static final NewCurveStyleState staticState = new NewCurveStyleState();
public CurveRenderState(float scale, HitObject hitObject) {
fbo = null;
this.scale = scale;
this.hitObject = hitObject;
}
public void draw(Color color, Vec2f[] curve) {
float alpha = color.a;
//if this curve hasn't been drawn, draw it and cache the result
if (fbo == null) {
FrameBufferCache cache = FrameBufferCache.getInstance();
Rendertarget mapping = cache.get(hitObject);
if (mapping == null) {
mapping = cache.insert(hitObject);
}
fbo = mapping;
int old_fb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING);
int old_tex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID());
GL11.glViewport(0, 0, fbo.width, fbo.height);
GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
Utils.COLOR_WHITE_FADE.a = 1.0f;
this.draw_curve(color, curve);
color.a = 1f;
GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_tex);
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_fb);
Utils.COLOR_WHITE_FADE.a = alpha;
}
//draw a fullscreen quad with the texture that contains the curve
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glDisable(GL11.GL_TEXTURE_1D);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.getTextureID());
GL11.glBegin(GL11.GL_QUADS);
GL11.glColor4f(1.0f, 1.0f, 1.0f, alpha);
GL11.glTexCoord2f(1.0f, 1.0f);
GL11.glVertex2i(fbo.width, 0);
GL11.glTexCoord2f(0.0f, 1.0f);
GL11.glVertex2i(0, 0);
GL11.glTexCoord2f(0.0f, 0.0f);
GL11.glVertex2i(0, fbo.height);
GL11.glTexCoord2f(1.0f, 0.0f);
GL11.glVertex2i(fbo.width, fbo.height);
GL11.glEnd();
}
public void discardCache() {
fbo = null;
FrameBufferCache.getInstance().freeMappingFor(hitObject);
}
private class RenderState {
boolean smoothedPoly;
boolean blendEnabled;
boolean depthEnabled;
boolean depthWriteEnabled;
boolean texEnabled;
int texUnit;
int oldProgram;
int oldArrayBuffer;
}
private RenderState startRender() {
RenderState state = new RenderState();
state.smoothedPoly = GL11.glGetBoolean(GL11.GL_POLYGON_SMOOTH);
state.blendEnabled = GL11.glGetBoolean(GL11.GL_BLEND);
state.depthEnabled = GL11.glGetBoolean(GL11.GL_DEPTH_TEST);
state.depthWriteEnabled = GL11.glGetBoolean(GL11.GL_DEPTH_WRITEMASK);
state.texEnabled = GL11.glGetBoolean(GL11.GL_TEXTURE_2D);
state.texUnit = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE);
state.oldProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM);
state.oldArrayBuffer = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING);
GL11.glDisable(GL11.GL_POLYGON_SMOOTH);
GL11.glEnable(GL11.GL_BLEND);
GL14.glBlendEquation(GL14.GL_FUNC_ADD);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glEnable(GL11.GL_DEPTH_TEST);
GL11.glDepthMask(true);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glEnable(GL11.GL_TEXTURE_1D);
GL11.glBindTexture(GL11.GL_TEXTURE_1D, staticState.gradientTexture);
GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);
GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(GL11.GL_TEXTURE_1D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP);
GL20.glUseProgram(0);
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glPushMatrix();
GL11.glLoadIdentity();
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glPushMatrix();
GL11.glLoadIdentity();
return state;
}
private void endRender(RenderState state) {
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glPopMatrix();
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glPopMatrix();
GL11.glEnable(GL11.GL_BLEND);
GL20.glUseProgram(state.oldProgram);
GL13.glActiveTexture(state.texUnit);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, state.oldArrayBuffer);
if (!state.depthWriteEnabled) {
GL11.glDepthMask(false);
}
if (!state.depthEnabled) {
GL11.glDisable(GL11.GL_DEPTH_TEST);
}
if (state.texEnabled) {
GL11.glEnable(GL11.GL_TEXTURE_2D);
}
if (state.smoothedPoly) {
GL11.glEnable(GL11.GL_POLYGON_SMOOTH);
}
if (!state.blendEnabled) {
GL11.glDisable(GL11.GL_BLEND);
}
}
/**
* Do the actual drawing of the curve into the currently bound framebuffer.
* @param color the color of the curve
* @param curve the points along the curve
*/
private void draw_curve(Color color, Vec2f[] curve) {
RenderState state = startRender();
int vtx_buf;
//floatsize * (position + texture coordinates) * (number of cones) * (vertices in a cone)
FloatBuffer buff = BufferUtils.createByteBuffer(4 * (4 + 2) * (2 * curve.length - 1) * (NewCurveStyleState.DIVIDES + 2)).asFloatBuffer();
staticState.initShaderProgram();
staticState.initGradient();
vtx_buf = GL15.glGenBuffers();
for (int i = 0; i < curve.length; ++i) {
float x = curve[i].x;
float y = curve[i].y;
//if(i == 0 || i==curve.length-1){
fillCone(buff, x, y, NewCurveStyleState.DIVIDES);
if (i != 0) {
float last_x = curve[i - 1].x;
float last_y = curve[i - 1].y;
double diff_x = x - last_x;
double diff_y = y - last_y;
x = (float) (x - diff_x / 2);
y = (float) (y - diff_y / 2);
fillCone(buff, x, y, NewCurveStyleState.DIVIDES);
}
}
buff.flip();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vtx_buf);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buff, GL15.GL_STATIC_DRAW);
staticState.render(color, curve.length * 2 - 1, vtx_buf);
GL15.glDeleteBuffers(vtx_buf);
endRender(state);
}
/**
* Fill {@code buff} with the texture coordinates and positions for a cone
* with {@code DIVIDES} ground corners that has its center at the coordinates
* {@code (x1,y1)}
* @param buff the buffer to be filled
* @param x1 x-coordinate of the cone
* @param y1 y-coordinate of the cone
* @param DIVIDES the base of the cone is a regular polygon with this many sides
*/
protected void fillCone(FloatBuffer buff, float x1, float y1, final int DIVIDES) {
float divx = Options.getLatestResolutionWidth() / 2.0f;
float divy = Options.getLatestResolutionHeight() / 2.0f;
float offx = -1.0f;
float offy = 1.0f;
float x, y;
float radius = scale / 2;
buff.put(1.0f);
buff.put(0.5f);
//GL11.glTexCoord2d(1.0, 0.5);
x = offx + x1 / divx;
y = offy - y1 / divy;
buff.put(x);
buff.put(y);
buff.put(0f);
buff.put(1f);
//GL11.glVertex4f(x, y, 0.0f, 1.0f);
for (int j = 0; j < DIVIDES; ++j) {
double phase = j * (float) Math.PI * 2 / DIVIDES;
buff.put(0.0f);
buff.put(0.5f);
//GL11.glTexCoord2d(0.0, 0.5);
x = (x1 + radius * (float) Math.sin(phase)) / divx;
y = (y1 + radius * (float) Math.cos(phase)) / divy;
buff.put((offx + x));
buff.put((offy - y));
buff.put(1f);
buff.put(1f);
//GL11.glVertex4f(x + 90 * (float) Math.sin(phase), y + 90 * (float) Math.cos(phase), 1.0f, 1.0f);
}
buff.put(0.0f);
buff.put(0.5f);
//GL11.glTexCoord2d(0.0, 0.5);
x = (x1 + radius * (float) Math.sin(0.0)) / divx;
y = (y1 + radius * (float) Math.cos(0.0)) / divy;
buff.put((offx + x));
buff.put((offy - y));
buff.put(1f);
buff.put(1f);
//GL11.glVertex4f(x + 90 * (float) Math.sin(0.0), y + 90 * (float) Math.cos(0.0), 1.0f, 1.0f);
}
/**
* Contains all the necessary state that needs to be tracked to draw curves
* in the new style and not re-create the shader each time.
*
* @author Bigpet {@literal <dravorek@gmail.com>}
*/
private static class NewCurveStyleState {
/**
* used for new style Slider rendering, defines how many vertices the
* base of the cone has that is used to draw the curve
*/
protected static final int DIVIDES = 30;
/**
* OpenGL shader program ID used to draw and recolor the curve
*/
protected int program = 0;
/**
* OpenGL shader attribute location of the vertex position attribute
*/
protected int attribLoc = 0;
/**
* OpenGL shader attribute location of the texture coordinate attribute
*/
protected int texCoordLoc = 0;
/**
* OpenGL shader uniform location of the color attribute
*/
protected int colLoc = 0;
/**
* OpenGL texture id for the gradient texture for the curve
*/
protected int gradientTexture = 0;
/**
* Reads the first row of the slider gradient texture and upload it as
* a 1D texture to OpenGL if it hasn't already been done
*/
public void initGradient()
{
if(gradientTexture == 0)
{
Image slider = GameImage.SLIDER_GRADIENT.getImage();
staticState.gradientTexture = GL11.glGenTextures();
ByteBuffer buff = BufferUtils.createByteBuffer(slider.getWidth()*4);
for(int i=0 ; i< slider.getWidth(); ++i)
{
Color col = slider.getColor(i, 0);
buff.put((byte)(255*col.r));
buff.put((byte)(255*col.g));
buff.put((byte)(255*col.b));
buff.put((byte)(255*col.a));
}
buff.flip();
GL11.glBindTexture(GL11.GL_TEXTURE_1D, gradientTexture);
GL11.glTexImage1D(GL11.GL_TEXTURE_1D,0,GL11.GL_RGBA,slider.getWidth(),0,GL11.GL_RGBA,GL11.GL_UNSIGNED_BYTE,buff);
GL30.glGenerateMipmap(GL11.GL_TEXTURE_1D);
}
}
/**
* Compiles and links the shader program for the new style curve objects
* if it hasn't already been compiled and linked.
*/
public void initShaderProgram() {
if (program == 0) {
program = GL20.glCreateProgram();
int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
GL20.glShaderSource(vtxShdr, "#version 330\n"
+ "\n"
+ "layout(location = 0) in vec4 in_position;\n"
+ "layout(location = 1) in vec2 in_tex_coord;\n"
+ "\n"
+ "out vec2 tex_coord;\n"
+ "void main()\n"
+ "{\n"
+ " gl_Position = in_position;\n"
+ " tex_coord = in_tex_coord;\n"
+ "}");
GL20.glCompileShader(vtxShdr);
int res = GL20.glGetShaderi(vtxShdr, GL20.GL_COMPILE_STATUS);
if (res != GL11.GL_TRUE) {
String error = GL20.glGetShaderInfoLog(vtxShdr, 1024);
Log.error("Vertex Shader compilation failed", new Exception(error));
}
GL20.glShaderSource(frgShdr, "#version 330\n"
+ "\n"
+ "uniform sampler1D tex;\n"
+ "uniform vec2 tex_size;\n"
+ "uniform vec3 col_tint;\n"
+ "\n"
+ "in vec2 tex_coord;\n"
+ "layout(location = 0) out vec4 out_colour;\n"
+ "\n"
+ "void main()\n"
+ "{\n"
+ " vec4 in_color = texture(tex, tex_coord.x);\n"
+ " float blend_factor = in_color.r-in_color.b;\n"
+ " vec4 new_color = vec4(mix(in_color.xyz,col_tint,blend_factor),in_color.w);\n"
+ " out_colour = new_color;\n"
+ "}");
GL20.glCompileShader(frgShdr);
res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS);
if (res != GL11.GL_TRUE) {
String error = GL20.glGetShaderInfoLog(frgShdr, 1024);
Log.error("Fragment Shader compilation failed", new Exception(error));
}
GL20.glAttachShader(program, vtxShdr);
GL20.glAttachShader(program, frgShdr);
GL20.glLinkProgram(program);
res = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS);
if (res != GL11.GL_TRUE) {
String error = GL20.glGetProgramInfoLog(program, 1024);
Log.error("Program linking failed", new Exception(error));
}
attribLoc = GL20.glGetAttribLocation(program, "in_position");
texCoordLoc = GL20.glGetAttribLocation(program, "in_tex_coord");
int texLoc = GL20.glGetUniformLocation(program, "tex");
colLoc = GL20.glGetUniformLocation(program, "col_tint");
GL20.glUniform1i(texLoc, 0);
}
}
/**
* Make the drawcalls for the curve. This requires the {@code vertexBufferObject} to be filled with
* {@code numCones * (NewCurveStyleState.DIVIDES+2)} sets of (u,v,x,y,z,w) floats
* @param color the color of the curve to be drawn
* @param numCones the number of points in the {code vertexBufferObject}
* @param vertexBufferObject the OpenGL vertex buffer object ID that
* contains the positions and texture coordinates of the cones
*/
public void render(Color color, int numCones, int vertexBufferObject) {
GL20.glUseProgram(program);
GL20.glEnableVertexAttribArray(attribLoc);
GL20.glEnableVertexAttribArray(texCoordLoc);
//stride is 6*4 for the floats (4 bytes) (u,v)(x,y,z,w)
//2*4 is for skipping the first 2 floats (u,v)
GL20.glVertexAttribPointer(attribLoc, 4, GL11.GL_FLOAT, false, 6 * 4, 2 * 4);
GL20.glVertexAttribPointer(texCoordLoc, 2, GL11.GL_FLOAT, false, 6 * 4, 0);
GL20.glUniform3f(colLoc, color.r, color.g, color.b);
for (int i = 0; i < numCones; ++i) {
GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, i * (NewCurveStyleState.DIVIDES + 2), NewCurveStyleState.DIVIDES + 2);
}
GL20.glDisableVertexAttribArray(texCoordLoc);
GL20.glDisableVertexAttribArray(attribLoc);
}
}
}

View File

@ -0,0 +1,123 @@
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.render;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.beatmap.HitObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.newdawn.slick.util.Log;
/**
* This is cache for OpenGL FrameBufferObjects. This is currently only used
* to draw curve objects of the new slider style. Does currently not integrate
* well and requires some manual OpenGL state manipulation to use it.
* @author Bigpet {@literal <dravorek@gmail.com>}
*/
public class FrameBufferCache {
private static final int INITIAL_CACHE_SIZE = 4;
private static FrameBufferCache instance = null;
private Map<HitObject, Rendertarget> cacheMap;
private ArrayList<Rendertarget> cache;
public final int width;
public final int height;
private FrameBufferCache(int width, int height) {
cache = new ArrayList<>(INITIAL_CACHE_SIZE);
cacheMap = new HashMap<>();
this.width = width;
this.height = height;
for (int i = 0; i < INITIAL_CACHE_SIZE; ++i) {
cache.add(Rendertarget.createRTTFramebuffer(width, height));
}
}
/**
* Check if there is a framebuffer object mapped to the {@code HitObject obj}
* @param obj
* @return true if there is a framebuffer mapped for this HitObject, else false
*/
public boolean contains(HitObject obj) {
return cacheMap.containsKey(obj);
}
/**
* Get the {@code Rendertarget} mapped to {@code obj}
* @param obj
* @return the {@code Rendertarget} if there's one mapped to {@code obj}, otherwise null
*/
public Rendertarget get(HitObject obj) {
return cacheMap.get(obj);
}
/**
* Clear the mapping for {@code obj} to free it up to get used by another {@code HitObject}
* @param obj
* @return true if there was a mapping for {@code obj} and false if there was no mapping for it.
*/
public boolean freeMappingFor(HitObject obj) {
return cacheMap.remove(obj) != null;
}
/**
* Clear the cache of all the mappings. This does not actually delete the
* cached framebuffers, it merely frees them all up to get mapped anew.
*/
public void freeMap() {
cacheMap.clear();
}
/**
* Create a mapping from {@code obj} to a framebuffer. If there was already
* a mapping from {@code obj} this will associate another framebuffer with it
* (thereby freeing up the previously mapped framebuffer).
* @param obj
* @return the {@code Rendertarget} newly mapped to {@code obj}
*/
public Rendertarget insert(HitObject obj) {
//find first RTTFramebuffer that's not mapped to anything and return it
Rendertarget buffer;
for (int i = 0; i < cache.size(); ++i) {
buffer = cache.get(i);
if (!cacheMap.containsValue(buffer)) {
cacheMap.put(obj, buffer);
return buffer;
}
}
//no unmapped RTTFramebuffer found, create a new one
buffer = Rendertarget.createRTTFramebuffer(width, height);
cache.add(buffer);
Log.warn("Framebuffer cache was not large enough, possible resource leak.");
cacheMap.put(obj, buffer);
return buffer;
}
/**
* There should only ever be one framebuffer cache, this function returns that one framebuffer cache instance.
* If there was no instance created already then this function creates it.
* @return the instance of FrameBufferCache
*/
public static FrameBufferCache getInstance() {
if (instance == null) {
instance = new FrameBufferCache(Options.getLatestResolutionWidth(), Options.getLatestResolutionHeight());
}
return instance;
}
}

View File

@ -0,0 +1,110 @@
/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.render;
import java.nio.ByteBuffer;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL32;
/**
*
* Represents a rendertarget. For now this maps to an OpenGL FBO via LWJGL
*/
public class Rendertarget {
public final int width;
public final int height;
private final int fboID;
private final int textureID;
/**
* Create a new FBO
*
* @param width
* @param height
*/
private Rendertarget(int width, int height)
{
this.width = width;
this.height = height;
fboID = GL30.glGenFramebuffers();
textureID = GL11.glGenTextures();
}
/**
* Bind this rendertarget as the primary framebuffer.
*/
public void bind()
{
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fboID);
}
//use judiciously, try to avoid if possible and consider adding a method to
//this class if you find yourself calling this repeatedly
public int getID()
{
return fboID;
}
//try not to use, could be moved into seperate class
public int getTextureID()
{
return textureID;
}
/**
* Bind the default framebuffer
*/
public static void unbind()
{
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, 0);
}
/**
* Creates a Rendertarget with a Texture that it renders the color buffer in
* and a renderbuffer that it renders the depth to.
*/
public static Rendertarget createRTTFramebuffer(int width, int height)
{
int old_framebuffer = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING);
int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
Rendertarget buffer = new Rendertarget(width,height);
buffer.bind();
int fboTexture = buffer.textureID;
GL11.glBindTexture(GL11.GL_TEXTURE_2D, fboTexture);
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, 4, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_INT, (ByteBuffer) null);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
int fboDepth = GL30.glGenRenderbuffers();
GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, fboDepth);
GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT, width, height);
GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, fboDepth);
GL32.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, fboTexture, 0);
GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture);
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_framebuffer);
return buffer;
}
}

View File

@ -41,6 +41,7 @@ import itdelatrisu.opsu.objects.DummyObject;
import itdelatrisu.opsu.objects.GameObject;
import itdelatrisu.opsu.objects.Slider;
import itdelatrisu.opsu.objects.Spinner;
import itdelatrisu.opsu.render.FrameBufferCache;
import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
@ -1023,6 +1024,9 @@ public class Game extends BasicGameState {
if (beatmap == null || beatmap.objects == null)
throw new RuntimeException("Running game with no beatmap loaded.");
//@TODO: find a better place to clean the SliderCache
FrameBufferCache.getInstance().freeMap();
// grab the mouse (not working for touchscreen)
// container.setMouseGrabbed(true);

View File

@ -59,7 +59,8 @@ public class OptionsMenu extends BasicGameState {
GameOption.SCREENSHOT_FORMAT,
GameOption.NEW_CURSOR,
GameOption.DYNAMIC_BACKGROUND,
GameOption.LOAD_VERBOSE
GameOption.LOAD_VERBOSE,
GameOption.NEW_SLIDER
}),
MUSIC ("Music", new GameOption[] {
GameOption.MASTER_VOLUME,