Java-Gaming.org Hi !
Featured games (90)
games approved by the League of Dukes
Games in Showcase (794)
Games in Android Showcase (234)
games submitted by our members
Games in WIP (864)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: [1]
  ignore  |  Print  
  [libGDX] Subpixel smooth spite movement  (Read 1184 times)
0 Members and 1 Guest are viewing this topic.
Offline disengalp

Junior Newbie





« Posted 2019-03-27 15:48:19 »

Hello Guys,

Help needed !
I tried to achieve smooth sub-pixel scrolling for my pixel game.
Below approach works greate:
https://code-disaster.com/2016/02/subpixel-perfect-smooth-scrolling.html
https://github.com/code-disaster/gdx-mellow-demo/blob/master/core/src/com/codedisaster/mellow/MellowGame.java

But when I add sprite to MellowGame.java code and start to move that sprite across tilemap it turned out that it moves only in pixel intervals.
(e.g. i can't move that sprite in 1.5 pixels).

I am using tileRenderer's batch to draw sprite:
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
sceneFrameBuffer.begin();

tiledMapRenderer.setView(sceneCamera);
tiledMapRenderer.render();
batch.begin();
batch.setProjectionMatrix(sceneCamera.combined);
playerSprite.draw(tiledMapRenderer.getBatch());
batch.end();

sceneFrameBuffer.end();


Maybe someone know how can smooth sub-pixel sprite movement can be achieved with that upscaled approach?

Thanks in advance for help !


Offline disengalp

Junior Newbie





« Reply #1 - Posted 2019-03-27 15:51:44 »

Here is code I used (shaders were not modified):

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  
52  
53  
54  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
82  
83  
84  
85  
86  
87  
88  
89  
90  
91  
92  
93  
94  
95  
96  
97  
98  
99  
100  
101  
102  
103  
104  
105  
106  
107  
108  
109  
110  
111  
112  
113  
114  
115  
116  
117  
118  
119  
120  
121  
122  
123  
124  
125  
126  
127  
128  
129  
130  
131  
132  
133  
134  
135  
136  
137  
138  
139  
140  
141  
142  
143  
144  
145  
146  
147  
148  
149  
150  
151  
152  
153  
154  
155  
156  
157  
158  
159  
160  
161  
162  
163  
164  
165  
166  
167  
168  
169  
170  
171  
172  
173  
174  
175  
176  
177  
178  
179  
180  
181  
182  
183  
184  
185  
186  
187  
188  
189  
190  
191  
192  
193  
194  
195  
196  
197  
198  
199  
200  
201  
202  
203  
204  
205  
206  
207  
208  
209  
210  
211  
212  
213  
214  
215  
216  
217  
218  
219  
220  
221  
222  
223  
224  
225  
226  
227  
228  
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;
      }
   }

}
Offline CoDi^R
« Reply #2 - Posted 2019-03-28 10:09:34 »

But when I add sprite to MellowGame.java code and start to move that sprite across tilemap it turned out that it moves only in pixel intervals.
(e.g. i can't move that sprite in 1.5 pixels).

...

Maybe someone know how can smooth sub-pixel sprite movement can be achieved with that upscaled approach?

Well, that doesn't work because the upscaling/scrolling is done as a post-render effect, using the final frame rendered in low resolution.

I did a brief experiment with rendering "kind-a sub-pixel moving" sprites a while back. It's... difficult. One approach I tried is to render moving sprites into a separate frame buffer, at higher (4x, 8x) resolution, and then blend it accordingly before (final scene composition) or during the upscale/smooth-scroll render pass. My results were.. mixed. It looked quite promising, but at some point you are starting to fight with blending artifacts.

I believe the "easiest" method would be to render moving sprites at a scale matching the upscale factor. e.g. if the upscale shader does a x3 scaling, render the sprites into a frame buffer 3x the size of the primary render buffer. Then, in the upscale shader, mix/mask background with sprites, but before subsampling. That would not give you true "sub-pixel", but just "1/3 pixel" movement for sprites.

Robotality - steamworks4j - @code_disaster - codi^r @ #java-gaming
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline disengalp

Junior Newbie





« Reply #3 - Posted 2019-04-02 07:37:56 »

Hello, thanks for reply and approach described!
For now, I will not use that upscale approach. On one hand it gives smooth camera scrolling, but on another it tricky to achieve sub-pixel sprite movement, which is important for my game, since some objects can move very slowly (and without sub-pixel movement it looks discrete).
As workaround I do the following:
1) set width and height in units visible on screen (e.g. 10x6), 1 unit = 32 pixels (PPU).
2) set game resolution - 320x192 (10*32x6*32)
3) set upscale factor - 4 (SCALE_FACTOR), so game window resolution will be 1280x768 (320*4x192*4)
4) following code used for camera position calculation:
1  
2  
3  
4  
5  
6  
7  
        Vector2 cameraPosition = cameraSystem.getCameraPosition();
        float sceneX = cameraPosition.x;
        float sceneY = cameraPosition.y;
        float sceneXPxl = MathUtils.round(sceneX * Const.PPU * SCALE_FACTOR) / (Const.PPU * SCALE_FACTOR);
        float sceneYPxl = MathUtils.round(sceneY * Const.PPU * SCALE_FACTOR) / (Const.PPU * SCALE_FACTOR);
        cameraSystem.getCamera().position.set(sceneXPxl, sceneYPxl, 0.0f);
        cameraSystem.getCamera().update();

It snap (rounds) camera position to game 'window' pixels, which is 1/4 of game pixels. On very slow camera movement it is looks discrete.
5) Sprites still can move in sub-pixel intervals since no render to texture done with subsequent upscale render.

Pages: [1]
  ignore  |  Print  
 
 

 
hadezbladez (3162 views)
2018-11-16 13:46:03

hadezbladez (1145 views)
2018-11-16 13:41:33

hadezbladez (3131 views)
2018-11-16 13:35:35

hadezbladez (625 views)
2018-11-16 13:32:03

EgonOlsen (3913 views)
2018-06-10 19:43:48

EgonOlsen (4389 views)
2018-06-10 19:43:44

EgonOlsen (2618 views)
2018-06-10 19:43:20

DesertCoockie (3437 views)
2018-05-13 18:23:11

nelsongames (3559 views)
2018-04-24 18:15:36

nelsongames (4609 views)
2018-04-24 18:14:32
Java Gaming Resources
by philfrei
2019-05-14 16:15:13

Deployment and Packaging
by philfrei
2019-05-08 15:15:36

Deployment and Packaging
by philfrei
2019-05-08 15:13:34

Deployment and Packaging
by philfrei
2019-02-17 20:25:53

Deployment and Packaging
by mudlee
2018-08-22 18:09:50

Java Gaming Resources
by gouessej
2018-08-22 08:19:41

Deployment and Packaging
by gouessej
2018-08-22 08:04:08

Deployment and Packaging
by gouessej
2018-08-22 08:03:45
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!