Here is code I used (shaders were not modified):
package com.codedisaster.mellow;
import com.badlogic.gdx.*;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.graphics.g2d.*;
import com.badlogic.gdx.graphics.glutils.*;
import com.badlogic.gdx.maps.tiled.TiledMap;
import com.badlogic.gdx.maps.tiled.TmxMapLoader;
import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer;
import com.badlogic.gdx.math.*;
import static com.badlogic.gdx.Input.Keys;
public class MellowGame extends ApplicationAdapter {
private static final float PIXELS_IN_UNIT = 16.0f;
private static final float PIXELS_IN_UNIT_INV = 1.0f / PIXELS_IN_UNIT;
private static final float CAMERA_WIDTH_UNITS = 16f;
private static final float CAMERA_HEIGHT_UNITS = 12f;
private static final int FRAMEBUFFER_WIDTH_PXL = (int) (CAMERA_WIDTH_UNITS * PIXELS_IN_UNIT);
private static final int FRAMEBUFFER_HEIGHT_PXL = (int) (CAMERA_HEIGHT_UNITS * PIXELS_IN_UNIT);
private static final int UPSCALE = 4;
public static final int SCREEN_WIDTH_PXL = FRAMEBUFFER_WIDTH_PXL * UPSCALE;
public static final int SCREEN_HEIGHT_PXL = FRAMEBUFFER_HEIGHT_PXL * UPSCALE;
/*
Acceleration and dampen factors to make WASD movement non-linear.
*/
private SpriteBatch batch;
private ShaderProgram mellowShader;
private TiledMap tiledMap;
private OrthogonalTiledMapRenderer tiledMapRenderer;
private FrameBuffer sceneFrameBuffer;
private OrthographicCamera sceneCamera;
private Matrix4 screenProjectionMatrix = new Matrix4();
private Vector2 cameraPosition = new Vector2();
private Vector2 cameraDirection = new Vector2();
private static final float SPEED = 0.05f;
private static final float INV_SPEED = 1.0f - SPEED;
private Vector2 playerPosition = new Vector2();
private Vector2 playerPositionScaled = new Vector2();
private Vector2 playerVelocity = new Vector2();
private Sprite playerSprite;
@Override
public void create () {
batch = new SpriteBatch();
mellowShader = new ShaderProgram(Gdx.files.internal("shaders/mellow.vsh"), Gdx.files.internal("shaders/mellow.fsh"));
tiledMap = new TmxMapLoader().load("maps/test2.tmx");
tiledMapRenderer = new OrthogonalTiledMapRenderer(tiledMap, PIXELS_IN_UNIT_INV, batch);
int mapWidth = tiledMap.getProperties().get("width", int.class);
int mapHeight = tiledMap.getProperties().get("height", int.class);
sceneFrameBuffer = new FrameBuffer(Pixmap.Format.RGBA8888, FRAMEBUFFER_WIDTH_PXL, FRAMEBUFFER_HEIGHT_PXL, false);
sceneFrameBuffer.getColorBufferTexture().setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
// hero
Texture t = new Texture("hero.png");
playerPosition.set(0.5f * mapWidth, 0.5f * mapHeight);
float width = t.getWidth() * PIXELS_IN_UNIT_INV;
float height = t.getHeight() * PIXELS_IN_UNIT_INV;
float x = playerPosition.x - width/2;
float y = playerPosition.y - height/2;
playerSprite = new Sprite(t);
playerSprite.setBounds(x, y, width, height);
// initial camera position at center of map
cameraPosition.set(0.5f * mapWidth, 0.5f * mapHeight);
sceneCamera = new OrthographicCamera(CAMERA_WIDTH_UNITS, CAMERA_HEIGHT_UNITS);
sceneCamera.position.set(cameraPosition, 0f);
sceneCamera.update();
//Gdx.graphics.getWidth() - 1024, Gdx.graphics.getHeight() - 768
screenProjectionMatrix.setToOrtho2D(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
// input
Gdx.input.setInputProcessor(new Input());
}
@Override
public void render () {
float dT = Gdx.graphics.getDeltaTime();
float dx = playerVelocity.x * dT * 10;
float dy = playerVelocity.y * dT * 10;
playerPosition.add(dx, dy);
playerSprite.setPosition(playerPosition.x, playerPosition.y);
playerPositionScaled.set(playerPosition.x, playerPosition.y);
cameraPosition.scl(INV_SPEED);
playerPositionScaled.scl(SPEED);
cameraPosition.add(playerPositionScaled.x, playerPositionScaled.y);
float sceneX = cameraPosition.x;
float sceneY = cameraPosition.y;
// snap camera position to full pixels, avoiding floating point error artifacts
float sceneXPxl = MathUtils.floor(sceneX * PIXELS_IN_UNIT) / PIXELS_IN_UNIT;
float sceneYPxl = MathUtils.floor(sceneY * PIXELS_IN_UNIT) / PIXELS_IN_UNIT;
// set camera for rendering to snapped position
sceneCamera.position.set(sceneXPxl, sceneYPxl, 0.0f);
sceneCamera.update();
// calculate displacement offset: for UPSCALE=4, this results in (integer) offsets in [0..3]
float upscaleOffsetX = (sceneX - sceneXPxl) * PIXELS_IN_UNIT * UPSCALE;
float upscaleOffsetY = (sceneY - sceneYPxl) * PIXELS_IN_UNIT * UPSCALE;
// subpixel interpolation in [0..1]: basically the delta between two displacement offset values
float subpixelX = upscaleOffsetX - MathUtils.floor(upscaleOffsetX);
float subpixelY = upscaleOffsetY - MathUtils.floor(upscaleOffsetY);
upscaleOffsetX -= subpixelX;
upscaleOffsetY -= subpixelY;
// render tilemap to framebuffer
Gdx.gl20.glEnable(GL20.GL_SCISSOR_TEST); // re-enabled each frame because UI changes GL state
HdpiUtils.glScissor(0, 0, SCREEN_WIDTH_PXL, SCREEN_HEIGHT_PXL);
Gdx.gl.glClearColor(0.0f, 0.0f, 0.3f, 1.0f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
sceneFrameBuffer.begin();
tiledMapRenderer.setView(sceneCamera);
tiledMapRenderer.render();
batch.begin();
batch.setProjectionMatrix(sceneCamera.combined);
playerSprite.draw(tiledMapRenderer.getBatch());
batch.end();
sceneFrameBuffer.end();
// render upscaled framebuffer to backbuffer
// viewport/scissor adjust for artifacts at right/top pixel columns/lines
HdpiUtils.glViewport(UPSCALE / 2, UPSCALE / 2, SCREEN_WIDTH_PXL, SCREEN_HEIGHT_PXL);
HdpiUtils.glScissor(UPSCALE / 2, UPSCALE / 2, SCREEN_WIDTH_PXL - UPSCALE, SCREEN_HEIGHT_PXL - UPSCALE);
batch.begin();
batch.setShader(mellowShader);
batch.setProjectionMatrix(screenProjectionMatrix);
mellowShader.setUniformf("u_textureSizes", FRAMEBUFFER_WIDTH_PXL, FRAMEBUFFER_HEIGHT_PXL, UPSCALE, 0.0f);
mellowShader.setUniformf("u_sampleProperties", subpixelX, subpixelY, upscaleOffsetX, upscaleOffsetY);
batch.draw(sceneFrameBuffer.getColorBufferTexture(), 0, SCREEN_HEIGHT_PXL, SCREEN_WIDTH_PXL, -SCREEN_HEIGHT_PXL);
batch.end();
// reset scissor
batch.setShader(null);
HdpiUtils.glScissor(0, 0, SCREEN_WIDTH_PXL, SCREEN_HEIGHT_PXL);
}
@Override
public void dispose() {
Gdx.input.setInputProcessor(null);
tiledMapRenderer.dispose();
tiledMap.dispose();
mellowShader.dispose();
batch.dispose();
}
private class Input extends InputAdapter {
@Override
public boolean keyDown(int keycode) {
float value = 0.05f;
switch (keycode) {
case Keys.A:
MellowGame.this.cameraDirection.x = -value;
MellowGame.this.playerVelocity.set(-value, 0);
return true;
case Keys.D:
MellowGame.this.cameraDirection.x = value;
MellowGame.this.playerVelocity.set(value, 0);
return true;
case Keys.W:
MellowGame.this.cameraDirection.y = value;
MellowGame.this.playerVelocity.set(0, value);
return true;
case Keys.S:
MellowGame.this.cameraDirection.y = -value;
MellowGame.this.playerVelocity.set(0, -value);
return true;
}
return false;
}
@Override
public boolean keyUp(int keycode) {
MellowGame.this.playerVelocity.set(0, 0);
switch (keycode) {
case Keys.A:
MellowGame.this.cameraDirection.x = 0.0f;
return true;
case Keys.D:
MellowGame.this.cameraDirection.x = 0.0f;
return true;
case Keys.W:
MellowGame.this.cameraDirection.y = 0.0f;
return true;
case Keys.S:
MellowGame.this.cameraDirection.y = 0.0f;
return true;
}
return false;
}
}
}