diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index 3a44b81d8..1676307dc 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -239,13 +239,11 @@ public class App implements ApplicationListener { public static ShaderProgram shaderColLUT; public static ShaderProgram shaderReflect; public static ShaderProgram shaderGhastlyWhite; - public static ShaderProgram shaderHQ2x; - - private static Texture hq2xLut; + public static Hq2x hq2x; public static Mesh fullscreenQuad; public static Mesh fullscreenQuad2x; - public static Mesh fullscreenQuad2d; + public static Mesh fullscreenQuadHQnX; private static OrthographicCamera camera; private static FlippingSpriteBatch logoBatch; public static TextureRegion logo; @@ -478,9 +476,7 @@ public class App implements ApplicationListener { ); shaderPassthruRGBA = loadShaderFromClasspath("shaders/gl32spritebatch.vert", "shaders/gl32spritebatch.frag"); shaderReflect = loadShaderFromClasspath("shaders/default.vert", "shaders/reflect.frag"); - shaderHQ2x = loadShaderFromClasspath("shaders/hq2x.vert", "shaders/hq2x.frag"); - - hq2xLut = new Texture(Gdx.files.classpath("shaders/hq2x.tga")); + hq2x = new Hq2x(2); fullscreenQuad = new Mesh( true, 4, 6, @@ -494,7 +490,7 @@ public class App implements ApplicationListener { VertexAttribute.ColorUnpacked(), VertexAttribute.TexCoords(0) ); - fullscreenQuad2d = new Mesh( + fullscreenQuadHQnX = new Mesh( true, 4, 6, VertexAttribute.Position(), VertexAttribute.ColorUnpacked(), @@ -502,7 +498,7 @@ public class App implements ApplicationListener { ); updateFullscreenQuad(fullscreenQuad, scr.getWidth(), scr.getHeight()); updateFullscreenQuad(fullscreenQuad2x, scr.getWidth() * 2, scr.getHeight() * 2); - updateFullscreenQuad(fullscreenQuad2d, scr.getWidth() / 2, scr.getHeight() / 2); + updateFullscreenQuad(fullscreenQuadHQnX, Math.round(scr.getWf() * 1280f / 2028f), Math.round(scr.getHf() * 720f / 982f)); // set up renderer info variables renderer = Gdx.graphics.getGLVersion().getRendererString(); @@ -594,55 +590,18 @@ public class App implements ApplicationListener { }*/ - if (getConfigString("screenmagnifyingfilter").equals("hq2x")) { - int canvasWidth = postProcessorOutFBO.getWidth(); // not zoomed dimension - int canvasHeight = postProcessorOutFBO.getHeight(); - + if (getConfigString("screenmagnifyingfilter").equals("hq2x") ) { FrameBufferManager.begin(postProcessorOutFBO2); - - shaderHQ2x.bind(); - shaderHQ2x.setUniformMatrix("u_projTrans", camera.combined); - shaderHQ2x.setUniformi("u_lut", 1); - shaderHQ2x.setUniformi("u_texture", 0); - shaderHQ2x.setUniformf("u_textureSize", canvasWidth, canvasHeight); - hq2xLut.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest); - hq2xLut.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat); - hq2xLut.bind(1); - postProcessorOutFBO.getColorBufferTexture().setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest); - postProcessorOutFBO.getColorBufferTexture().setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat); - postProcessorOutFBO.getColorBufferTexture().bind(0); - fullscreenQuad.render(shaderHQ2x, GL20.GL_TRIANGLES); // the shader expects the target texture size to be 2x the input dimension - + hq2x.renderToScreen(postProcessorOutFBO.getColorBufferTexture()); FrameBufferManager.end(); - if (screenshotRequested) { - FrameBufferManager.begin(postProcessorOutFBO2); - try { - Pixmap p = Pixmap.createFromFrameBuffer(0, 0, postProcessorOutFBO2.getWidth(), postProcessorOutFBO2.getHeight()); - PixmapIO.writePNG(Gdx.files.absolute(defaultDir+"/Screenshot-"+String.valueOf(System.currentTimeMillis())+".png"), p, 9, true); - p.dispose(); - Terrarum.INSTANCE.getIngame().sendNotification("Screenshot taken"); - } - catch (Throwable e) { - e.printStackTrace(); - Terrarum.INSTANCE.getIngame().sendNotification("Failed to take screenshot: "+e.getMessage()); - } - FrameBufferManager.end(); - screenshotRequested = false; - } - - - - shaderPassthruRGBA.bind(); - shaderPassthruRGBA.setUniformMatrix("u_projTrans", camera.combined); shaderPassthruRGBA.setUniformi("u_texture", 0); postProcessorOutFBO2.getColorBufferTexture().setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear); - postProcessorOutFBO2.getColorBufferTexture().setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat); postProcessorOutFBO2.getColorBufferTexture().bind(0); - fullscreenQuad.render(shaderPassthruRGBA, GL20.GL_TRIANGLES); + fullscreenQuadHQnX.render(shaderPassthruRGBA, GL20.GL_TRIANGLES); } else if (getConfigDouble("screenmagnifying") < 1.01 || getConfigString("screenmagnifyingfilter").equals("none")) { shaderPassthruRGBA.bind(); @@ -796,7 +755,7 @@ public class App implements ApplicationListener { TerrarumPostProcessor.INSTANCE.resize(scr.getWidth(), scr.getHeight()); updateFullscreenQuad(fullscreenQuad, scr.getWidth(), scr.getHeight()); updateFullscreenQuad(fullscreenQuad2x, scr.getWidth() * 2, scr.getHeight() * 2); - updateFullscreenQuad(fullscreenQuad2d, scr.getWidth() / 2, scr.getHeight() / 2); + updateFullscreenQuad(fullscreenQuadHQnX, Math.round(scr.getWf() * 1280f / 2028f), Math.round(scr.getHf() * 720f / 982f)); if (renderFBO == null || @@ -865,14 +824,12 @@ public class App implements ApplicationListener { shaderColLUT.dispose(); shaderReflect.dispose(); shaderGhastlyWhite.dispose(); - shaderHQ2x.dispose(); - - hq2xLut.dispose(); + hq2x.dispose(); CommonResourcePool.INSTANCE.dispose(); fullscreenQuad.dispose(); fullscreenQuad2x.dispose(); - fullscreenQuad2d.dispose(); + fullscreenQuadHQnX.dispose(); logoBatch.dispose(); batch.dispose(); // shapeRender.dispose(); diff --git a/src/net/torvald/terrarum/Hq2x.kt b/src/net/torvald/terrarum/Hq2x.kt new file mode 100644 index 000000000..ed07b69bc --- /dev/null +++ b/src/net/torvald/terrarum/Hq2x.kt @@ -0,0 +1,214 @@ +package net.torvald.terrarum + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.files.FileHandle +import com.badlogic.gdx.graphics.* +import com.badlogic.gdx.graphics.VertexAttributes.Usage +import com.badlogic.gdx.graphics.glutils.FrameBuffer +import com.badlogic.gdx.graphics.glutils.ShaderProgram +import com.badlogic.gdx.math.Matrix4 +import com.badlogic.gdx.utils.Disposable +import com.badlogic.gdx.utils.GdxRuntimeException + +/** + * [HQnX](https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms#hqnx_family) + * upscale algorithm GLSL implementation based on + * [CrossVR](https://github.com/CrossVR/hqx-shader/tree/master/glsl) project. + */ +class Hq2x : Disposable { + + companion object { + private const val TEXTURE_HANDLE0 = 0 + private const val TEXTURE_HANDLE1 = 1 + + private const val U_TEXTURE = "u_texture" + private const val U_LUT = "u_lut" + private const val U_TEXTURE_SIZE = "u_textureSize" + } + + private val mesh = ViewportQuadMesh( + VertexAttribute(Usage.Position, 2, "a_position"), + VertexAttribute(Usage.TextureCoordinates, 2, "a_texCoord0")) + + private val program: ShaderProgram + private val lutTexture: Texture + private val scaleFactor: Int + + private var dstBuffer: FrameBuffer? = null + private var dstWidth = 0 + private var dstHeight = 0 + + /** @param scaleFactor should be 2, 3 or 4 value. */ + constructor(scaleFactor: Int) { + if (scaleFactor !in 2..4) { + throw GdxRuntimeException("Scale factor should be 2, 3 or 4.") + } + + program = compileShader( + Gdx.files.classpath("shaders/hq2x.vert"), + Gdx.files.classpath("shaders/hq2x.frag"), + "#define SCALE ${scaleFactor}.0") + + lutTexture = Texture(Gdx.files.classpath("shaders/hq${scaleFactor}x.png")) + + this.scaleFactor = scaleFactor + } + + override fun dispose() { + mesh.dispose() + program.dispose() + lutTexture.dispose() + dstBuffer?.dispose() + } + + fun rebind() { + program.bind() + program.setUniformi(U_TEXTURE, TEXTURE_HANDLE0) + program.setUniformi(U_LUT, TEXTURE_HANDLE1) + program.setUniformf(U_TEXTURE_SIZE, + dstWidth / scaleFactor.toFloat(), + dstHeight / scaleFactor.toFloat()) + } + + fun renderToScreen(src: Texture) { + validate(src) + + lutTexture.bind(TEXTURE_HANDLE1) + src.bind(TEXTURE_HANDLE0) + src.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest) + + program.bind() + mesh.render(program) + } + + fun renderToBuffer(src: Texture): Texture { + validate(src) + validateDstBuffer() + + lutTexture.bind(TEXTURE_HANDLE1) + src.bind(TEXTURE_HANDLE0) + src.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest) + + dstBuffer!!.begin() + program.bind() + mesh.render(program) + dstBuffer!!.end() + + return dstBuffer!!.colorBufferTexture + } + + private fun validate(src: Texture) { + val targetWidth = src.width * scaleFactor + val targetHeight = src.height * scaleFactor + +// println("[Hq2x] $targetWidth x $targetHeight") + + if (dstWidth != targetWidth || dstHeight != targetHeight) { + dstWidth = targetWidth + dstHeight = targetHeight + rebind() + } + } + + private fun validateDstBuffer() { + if (dstBuffer == null || dstBuffer!!.width != dstWidth || dstBuffer!!.height != dstHeight) { + dstBuffer?.dispose() + dstBuffer = FrameBuffer(Pixmap.Format.RGB888, dstWidth, dstHeight, false) + dstBuffer!!.colorBufferTexture.setFilter( + Texture.TextureFilter.Nearest, + Texture.TextureFilter.Nearest) + } + } +} + +/** + * Encapsulates a fullscreen quad mesh. Geometry is aligned to the viewport corners. + * + * @author bmanuel + * @author metaphore + */ +private class ViewportQuadMesh : Disposable { + + companion object { + private const val VERT_SIZE = 16 + private const val X1 = 0 + private const val Y1 = 1 + private const val U1 = 2 + private const val V1 = 3 + private const val X2 = 4 + private const val Y2 = 5 + private const val U2 = 6 + private const val V2 = 7 + private const val X3 = 8 + private const val Y3 = 9 + private const val U3 = 10 + private const val V3 = 11 + private const val X4 = 12 + private const val Y4 = 13 + private const val U4 = 14 + private const val V4 = 15 + + private val verts: FloatArray + + init { + verts = FloatArray(VERT_SIZE) + + // Vertex coords + verts[X1] = -1f + verts[Y1] = -1f + verts[X2] = 1f + verts[Y2] = -1f + verts[X3] = 1f + verts[Y3] = 1f + verts[X4] = -1f + verts[Y4] = 1f + + // Tex coords + verts[U1] = 0f + verts[V1] = 0f + verts[U2] = 1f + verts[V2] = 0f + verts[U3] = 1f + verts[V3] = 1f + verts[U4] = 0f + verts[V4] = 1f + } + } + + private val mesh: Mesh + + constructor(vararg attributes: VertexAttribute) { + mesh = Mesh(true, 4, 0, *attributes) + mesh.setVertices(verts) + } + + override fun dispose() { + mesh.dispose() + } + + /** Renders the quad with the specified shader program. */ + fun render(program: ShaderProgram) { + mesh.render(program, GL20.GL_TRIANGLE_FAN, 0, 4) + } +} + +private fun compileShader(vertexFile: FileHandle, fragmentFile: FileHandle, defines: String): ShaderProgram { + val sb = StringBuilder() + sb.append("Compiling \"").append(vertexFile.name()).append('/').append(fragmentFile.name()).append('\"') + if (defines.isNotEmpty()) { + sb.append(" w/ (").append(defines.replace("\n", ", ")).append(")") + } + sb.append("...") + Gdx.app.log("HqnxEffect", sb.toString()) + + val srcVert = vertexFile.readString() + val srcFrag = fragmentFile.readString() + val shader = ShaderProgram( + "$defines\n$srcVert".trimIndent(), + "$defines\n$srcFrag".trimIndent()) + + if (!shader.isCompiled) { + throw GdxRuntimeException("Shader compilation error: ${vertexFile.name()}/${fragmentFile.name()}\n${shader.log}") + } + return shader +} \ No newline at end of file diff --git a/src/shaders/hq2x.frag b/src/shaders/hq2x.frag index 8b0b5ee1d..1a8361bc7 100644 --- a/src/shaders/hq2x.frag +++ b/src/shaders/hq2x.frag @@ -1,24 +1,24 @@ // This float value should be defined from the compiling code. // #define SCALE [2, 3, 4].0 -#version 150 #ifdef GL_ES - precision mediump float; +#define PRECISION mediump +precision PRECISION float; +precision PRECISION int; +#else +#define PRECISION #endif -#define SCALE 2.0 uniform sampler2D u_texture; uniform sampler2D u_lut; uniform vec2 u_textureSize; -in vec4 v_texCoord[4]; +varying vec4 v_texCoord[4]; const mat3 YUV_MATRIX = mat3(0.299, 0.587, 0.114, -0.169, -0.331, 0.5, 0.5, -0.419, -0.081); const vec3 YUV_THRESHOLD = vec3(48.0/255.0, 7.0/255.0, 6.0/255.0); const vec3 YUV_OFFSET = vec3(0, 0.5, 0.5); -out vec4 fragColor; - bool diff(vec3 yuv1, vec3 yuv2) { return any(greaterThan(abs((yuv1 + YUV_OFFSET) - (yuv2 + YUV_OFFSET)), YUV_THRESHOLD)); } @@ -41,24 +41,24 @@ void main() { float dx = v_texCoord[0].z; float dy = v_texCoord[0].w; - vec3 p1 = texture(u_texture, v_texCoord[0].xy).rgb; - vec3 p2 = texture(u_texture, v_texCoord[0].xy + vec2(dx, dy) * quad).rgb; - vec3 p3 = texture(u_texture, v_texCoord[0].xy + vec2(dx, 0) * quad).rgb; - vec3 p4 = texture(u_texture, v_texCoord[0].xy + vec2(0, dy) * quad).rgb; + vec3 p1 = texture2D(u_texture, v_texCoord[0].xy).rgb; + vec3 p2 = texture2D(u_texture, v_texCoord[0].xy + vec2(dx, dy) * quad).rgb; + vec3 p3 = texture2D(u_texture, v_texCoord[0].xy + vec2(dx, 0) * quad).rgb; + vec3 p4 = texture2D(u_texture, v_texCoord[0].xy + vec2(0, dy) * quad).rgb; // Use mat4 instead of mat4x3 here to support GLES. mat4 pixels = mat4(vec4(p1, 0.0), vec4(p2, 0.0), vec4(p3, 0.0), vec4(p4, 0.0)); - vec3 w1 = yuv * texture(u_texture, v_texCoord[1].xw).rgb; - vec3 w2 = yuv * texture(u_texture, v_texCoord[1].yw).rgb; - vec3 w3 = yuv * texture(u_texture, v_texCoord[1].zw).rgb; + vec3 w1 = yuv * texture2D(u_texture, v_texCoord[1].xw).rgb; + vec3 w2 = yuv * texture2D(u_texture, v_texCoord[1].yw).rgb; + vec3 w3 = yuv * texture2D(u_texture, v_texCoord[1].zw).rgb; - vec3 w4 = yuv * texture(u_texture, v_texCoord[2].xw).rgb; + vec3 w4 = yuv * texture2D(u_texture, v_texCoord[2].xw).rgb; vec3 w5 = yuv * p1; - vec3 w6 = yuv * texture(u_texture, v_texCoord[2].zw).rgb; + vec3 w6 = yuv * texture2D(u_texture, v_texCoord[2].zw).rgb; - vec3 w7 = yuv * texture(u_texture, v_texCoord[3].xw).rgb; - vec3 w8 = yuv * texture(u_texture, v_texCoord[3].yw).rgb; - vec3 w9 = yuv * texture(u_texture, v_texCoord[3].zw).rgb; + vec3 w7 = yuv * texture2D(u_texture, v_texCoord[3].xw).rgb; + vec3 w8 = yuv * texture2D(u_texture, v_texCoord[3].yw).rgb; + vec3 w9 = yuv * texture2D(u_texture, v_texCoord[3].zw).rgb; bvec3 pattern[3]; pattern[0] = bvec3(diff(w5, w1), diff(w5, w2), diff(w5, w3)); @@ -75,13 +75,9 @@ void main() { vec2 step = vec2(1.0) / vec2(256.0, 16.0 * (SCALE * SCALE)); vec2 offset = step / vec2(2.0); - vec4 weights = texture(u_lut, index * step + offset); + vec4 weights = texture2D(u_lut, index * step + offset); float sum = dot(weights, vec4(1)); vec3 res = (pixels * (weights / sum)).rgb; - fragColor = vec4(res.rgb, 1.0); - - - -// fragColor = vec4(v_texCoord[0].x, v_texCoord[0].y, 0.0, 1.0); + gl_FragColor.rgb = res; } \ No newline at end of file diff --git a/src/shaders/hq2x.png b/src/shaders/hq2x.png new file mode 100644 index 000000000..d1aa86c15 --- /dev/null +++ b/src/shaders/hq2x.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27470641a8ab900b8ae889f22bc8bffeda587d7243e14de47730d9f9abcfa4ac +size 2793 diff --git a/src/shaders/hq2x.tga b/src/shaders/hq2x.tga deleted file mode 100644 index 6bac9d43d..000000000 --- a/src/shaders/hq2x.tga +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5fa8cb307db13cddc020963e3f71836f963807f117688fa2b3694d5f5275af5e -size 65554 diff --git a/src/shaders/hq2x.vert b/src/shaders/hq2x.vert index 1c1157bf4..997648c3c 100644 --- a/src/shaders/hq2x.vert +++ b/src/shaders/hq2x.vert @@ -1,26 +1,25 @@ -#version 150 #ifdef GL_ES - precision mediump float; +#define PRECISION mediump +precision PRECISION float; +precision PRECISION int; +#else +#define PRECISION #endif -#define SCALE 1.0 -in vec4 a_position; -in vec2 a_texCoord0; +attribute vec4 a_position; +attribute vec2 a_texCoord0; -uniform mat4 u_projTrans; +uniform vec2 u_textureSize; -out vec2 u_textureSize; -out vec4 v_texCoord[4]; +varying vec4 v_texCoord[4]; void main() { - gl_Position = u_projTrans * a_position / SCALE; + gl_Position = a_position; vec2 ps = 1.0/u_textureSize; float dx = ps.x; float dy = ps.y; - vec2 a_texCoord00 = a_texCoord0 / SCALE; - // +----+----+----+ // | | | | // | w1 | w2 | w3 | @@ -33,8 +32,8 @@ void main() { // +----+----+----+ v_texCoord[0].zw = ps; - v_texCoord[0].xy = a_texCoord00.xy; - v_texCoord[1] = a_texCoord00.xxxy + vec4(-dx, 0, dx, -dy); // w1 | w2 | w3 - v_texCoord[2] = a_texCoord00.xxxy + vec4(-dx, 0, dx, 0); // w4 | w5 | w6 - v_texCoord[3] = a_texCoord00.xxxy + vec4(-dx, 0, dx, dy); // w7 | w8 | w9 + v_texCoord[0].xy = a_texCoord0.xy; + v_texCoord[1] = a_texCoord0.xxxy + vec4(-dx, 0, dx, -dy); // w1 | w2 | w3 + v_texCoord[2] = a_texCoord0.xxxy + vec4(-dx, 0, dx, 0); // w4 | w5 | w6 + v_texCoord[3] = a_texCoord0.xxxy + vec4(-dx, 0, dx, dy); // w7 | w8 | w9 } \ No newline at end of file diff --git a/src/shaders/hq3x.png b/src/shaders/hq3x.png new file mode 100644 index 000000000..030faea10 --- /dev/null +++ b/src/shaders/hq3x.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d83f491caf19255426749e1b7b91be3fe4910bada78c541523835602085d948d +size 4665 diff --git a/src/shaders/hq4x.png b/src/shaders/hq4x.png new file mode 100644 index 000000000..aa28f8be3 --- /dev/null +++ b/src/shaders/hq4x.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f61d7396aa22b8b06cd5cd563771da7ea819fb6c6d9e2e420f965f4b2a1bef1d +size 11913