Pixelated rendering in LibGDX

In this article I’m having a look at rendering 3D geometry and/or sprites in arbitrary low resolutions in LibGDX. I’m taking my previous article with the crate model imported from Blender and change it to render the view into a low-resolution framebuffer then display it on the screen so that it appears pixelated.

Screenshot of the ModelTutorial app with pixelated rendering.

Screenshot of the ModelTutorial app with pixelated rendering.

My previous article on using fixed screen coordinates might have been a bit misleading and according to search keywords some of my readers expected to find something else. As I mentioned in the second paragraph, it only changed the coordinate system and the aspect ratio of the viewport while keeping the rendering area high resolution. The program in this post on the other hand will give you proper pixelated retro rendering.

Rendering into a framebuffer

The method is quite simple, and it’s very easy to change the existing rendering code. Instead of directly rendering to the screen, we need to render the whole scene into a low-resolution texture using a FrameBuffer object, then display that texture stretched over the whole screen with GL_NEAREST texture filter to avoid smoothing and show sharp pixel edges.

Extending the modeltutorial project from my previous post, I added a couple of more member variables to the ModelTutorial class:

public class ModelTutorial implements ApplicationListener {
	FrameBuffer fbo;
	SpriteBatch fboBatch;
	...
}

To use framebuffer objects, you need to use OpenGL ES 2.0, which should already be set up in my previous post. The SpriteBatch will only be used to render the FBO’s texture onto the screen. Make sure to release resources properly when the application is closed by adding the following to the dispose() method:

	@Override
	public void dispose() {
		...
		fbo.dispose();
		fboBatch.dispose();
		...
	}

Next is a simple method to initialise the framebuffer:

	public void initializeFBO() {
		if(fbo != null) fbo.dispose();
		fbo = new FrameBuffer(Pixmap.Format.RGB888, screenWidth / 8, screenHeight / 8, true);
		fbo.getColorBufferTexture().setFilter(TextureFilter.Nearest, TextureFilter.Nearest);

		if(fboBatch != null) fboBatch.dispose();
		fboBatch = new SpriteBatch();
	}

You have to call this method on create() and resize() as well. The code checks if the objects have been initialised and disposes them first, then re-creates them. As resize() should not be a regularly happening event, it shouldn’t affect performance a lot.

The size of the framebuffer is set in the constructor. In this example I simply devided the screen width and height by 8 which will give 8×8 pixel square blocks in the final render. In case you’re using a fixed size FBO, you don’t have to re-create the FBO object on resize.

The last parameter is set to true, meaning that the framebuffer will have a same size depth buffer attached to it as well. Whether or not you need a depth buffer depends on what you want to render, just like when you’re drawing directly to the screen, you can decide if you need depth testing enabled or not. If you want to render sprites in a specific order, you should turn it off to save texture memory. For 3D geometry you almost always want to have it on to prevent overdraw. For this particular tutorial we could actually turn it off, as it’s rendering a single convex mesh with face normals properly set, and due to back-face culling, overdraw would never happen. But let’s just leave it on, just in case we ever want to use a more complex 3D model.

A new SpriteBatch object sets up its projection matrix to use Y-up screen coordinates by default. When the window is resized, that matrix has to be reset to match the new dimensions. As it is not happening frequently, it’s simpler to just dispose the object and create a new one.

With all required object set up, all that’s left is the rendering code:

	@Override
	public void render() {
		// Respond to user events and update the camera
		cameraController.update();

		// Start rendering into the framebuffer
		fbo.begin();

		// Clear the viewport
		Gdx.gl.glViewport(0, 0, fbo.getWidth(), fbo.getHeight());
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

		// Draw all model instances using the camera
		modelBatch.begin(camera);
		modelBatch.render(instances, environment);
		modelBatch.end();

		// Finish rendering into the framebuffer
		fbo.end();

		// Draw framebuffer to the screen
		fboBatch.begin();
		fboBatch.draw(fbo.getColorBufferTexture(), 0, 0, screenWidth, screenHeight, 0, 0, 1, 1);
		fboBatch.end();
	}

If you compare the rendering code to the original, you’ll see that not much have changed. Any rendering calls between fbo.begin() and fbo.end() will be automatically rendered into the framebuffer, not the screen. The only other change is when setting up the viewport, you’ll need to set it to the FBO’s size, not the screen’s size.

After calling fbo.end(), the last three lines will draw the FBO’s color buffer to the screen. If you just blit the texture to the screen as a sprite, it will appear flipped because of the default Y-up coordinate system, therefore I used the version of the SpriteBatch.draw() method which allows setting the UV coordinates manually. The parameters for the draw function in order are: texture, x and y coordinates to draw to the screen at, width and height in pixels, U and V texture coordinates for the top-left then for the bottom-right corner.

With these simple changes, you’ll get the nice pixelated image rendering as seen at the top of the post.

Performance

In theory it might be expected that in exchange for increased memory usage to store the framebuffer, you might improve performance by reducing the number of fragments (pixels) in the rendering area this way, saving on fill-rate for overdrawn polygons, as well as reducing the number of times the fragment shader needs to run. In reality however, graphics drivers on mobile devices might not have a very efficient implementation for the framebuffer, causing slow-downs and increased battery use. Even with the seemingly all-powerful phones on the market these days, make sure to test your app’s performance on a wide variety of targeted devices.

If you’re only rendering simple sprites, it is still advisable to just use a simple GL_NEAREST texture filter, or some simple custom fragment shader code.

Share Button
Posted in Blog Tagged with: , , ,
0 comments on “Pixelated rendering in LibGDX
1 Pings/Trackbacks for "Pixelated rendering in LibGDX"
  1. […] If you wish to render a 3D scene in low resolution, you will need to use other techniques, like rendering into a low-resolution framebuffer object. […]

Leave a Reply