ScreenshotNeo

BlogHow-to

How to Improve Screenshot Quality in LibGDX

Capture the correct framebuffer, handle alpha and high-DPI scaling, and save sharper LibGDX screenshots with reliable Java code and fixes.

By the ScreenshotNeo team1 October 20267 min read

Short answer: improve LibGDX screenshots in the render pipeline, not by enlarging the saved PNG. Capture the framebuffer at the dimensions actually rendered, use the back-buffer dimensions when required by the backend, make alpha opaque when layered transparency causes artifacts, and render to a suitably sized FrameBuffer when you need more pixels. Then write the result with PixmapIO and dispose the native-memory Pixmap.

The official workflow is Pixmap.createFromFrameBuffer(...), PixmapIO.writePNG(...), and pixmap.dispose(). See the libGDX screenshot guide.

1. Capture a clean screenshot in Java

This complete example captures the current back buffer, removes unexpected transparency, writes a PNG, and releases the native memory:

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.PixmapIO;
import com.badlogic.gdx.files.FileHandle;

public final class Screenshots {
    private Screenshots() {}

    public static void save(String fileName) {
        int width = Gdx.graphics.getBackBufferWidth();
        int height = Gdx.graphics.getBackBufferHeight();

        Pixmap pixmap = Pixmap.createFromFrameBuffer(0, 0, width, height);
        try {
            // Use this when the final image should be fully opaque.
            for (int y = 0; y < pixmap.getHeight(); y++) {
                for (int x = 0; x < pixmap.getWidth(); x++) {
                    int rgba = pixmap.getPixel(x, y);
                    pixmap.drawPixel(x, y, rgba | 0x000000ff);
                }
            }

            FileHandle output = Gdx.files.local(fileName);
            PixmapIO.writePNG(output, pixmap);
        } finally {
            pixmap.dispose();
        }
    }
}

Call Screenshots.save("screenshots/frame.png") after the frame has finished rendering. If your backend’s logical dimensions match the buffer being read, Gdx.graphics.getWidth() and getHeight() also work. Check both choices on every target backend.

2. Fix transparency and compositing

Layered transparent content can leave alpha values in the captured pixels. The result may look faded, dark, or partially invisible in an image viewer even though it looked correct on screen. The official guide explicitly recommends post-processing in this case and demonstrates setting every alpha byte to 255: “However, if your screens have layered transparency, you need to postprocess the screenshot to remove any transparency.” See the guide.

When to preserve alpha

  • Preserve alpha for sprites, icons, or composited assets that must remain transparent.
  • Force alpha to 255 for a normal game screenshot intended for sharing, testing, or comparison against an opaque background.
  • If the screenshot is wrong only where translucent textures overlap, inspect blending and clear the screen before drawing.

SpriteBatch blending and per-frame clearing affect the pixels that are captured. Review the SpriteBatch documentation and ensure the intended blend state is active before the capture.

3. Make the image sharper by rendering more pixels

A larger output file cannot recreate detail that was never rendered. First compare logical window size with physical framebuffer size. The Graphics API documentation exposes screen size, pixel density, framebuffer dimensions, and anti-aliasing information. Use those values to decide whether the capture is reading a low-resolution buffer.

System.out.println("logical: " + Gdx.graphics.getWidth() + "x" + Gdx.graphics.getHeight());
System.out.println("back buffer: " + Gdx.graphics.getBackBufferWidth()
        + "x" + Gdx.graphics.getBackBufferHeight());
System.out.println("density: " + Gdx.graphics.getDensity());
System.out.println("samples: " + Gdx.graphics.getBufferFormat().samples);

If the back buffer is larger than the logical window, capture using the back-buffer dimensions. If both are small, render the scene at a larger target size instead of scaling the finished PNG.

4. Render to a higher-resolution FrameBuffer

A FrameBuffer gives you a controlled render-to-texture path. Render the scene into the desired pixel dimensions, then read that result or draw it into the final output. The FrameBuffer documentation covers setup and notes that framebuffer textures are generally vertically flipped.

import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.graphics.Pixmap;

FrameBuffer fbo = new FrameBuffer(Pixmap.Format.RGBA8888, 1920, 1080, true);
fbo.begin();
try {
    Gdx.gl.glClearColor(0f, 0f, 0f, 1f);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
    renderSceneAt(1920, 1080);
} finally {
    fbo.end();
}

Texture texture = fbo.getColorBufferTexture();
// Draw texture with a vertically flipped TextureRegion when displaying it.
// Dispose fbo when the target resolution is no longer needed.

Choose dimensions supported by the target GPU. Larger buffers consume more GPU memory and increase fill, readback, and PNG-encoding time. The documentation’s 1024 x 720 example is illustrative, not a universal recommendation.

5. Handle high-DPI and HTML5 differences

On desktop and mobile, logical coordinates can differ from physical pixels. Depending on the backend, convert dimensions with HdpiUtils or configure rendering in pixel mode. Do not assume that a 1280-wide window produces a 1280-pixel buffer.

For HTML5, the documentation describes a mobile case where the reported size differs from the physical screen and points to config.usePhysicalPixels = true. See HTML5 backend and GWT specifics.

HdpiModeConfig config = new HdpiModeConfig();
config.usePhysicalPixels = true;

Use the exact configuration type and launcher setup for your libGDX version and backend. Verify the resulting getWidth(), getBackBufferWidth(), and screenshot dimensions at runtime.

6. Orientation, coordinate systems, and image viewers

Framebuffer textures are commonly upside down because OpenGL’s origin differs from typical image coordinates. A screenshot read directly from the screen and a texture read from a FrameBuffer can therefore need different handling. The coordinate-systems documentation explains these conventions.

If the saved PNG is upside down, flip the pixels before encoding or use a vertically flipped TextureRegion when displaying the framebuffer texture. Do not flip twice: verify with a labeled test scene containing text at the top and bottom.

7. A practical quality checklist

  1. Render the scene completely, then capture after the final draw call.
  2. Log logical and back-buffer dimensions on each device and backend.
  3. Capture the buffer whose dimensions match the pixels you intend to save.
  4. Use an opaque alpha channel unless transparency is part of the asset.
  5. Clear the color and depth buffers consistently each frame.
  6. Confirm blending state for translucent textures.
  7. Use a larger FrameBuffer when more rendered pixels are required.
  8. Check anti-aliasing capabilities through Graphics; availability depends on backend and device.
  9. Check orientation with a known test image.
  10. Dispose every temporary Pixmap and FrameBuffer.

8. Troubleshooting common problems

Symptom Likely cause Fix
Image is blurry or pixelated Scene was rendered at a low logical or physical resolution Compare logical and back-buffer sizes; render to a larger framebuffer. Upscaling the PNG alone cannot add detail.
Image looks faded or has unwanted transparency Layered content left alpha below 255 Post-process alpha to 255 for an opaque screenshot, or deliberately composite over a background.
Screenshot is upside down Framebuffer/OpenGL origin differs from image coordinates Flip once during pixel processing or when drawing the framebuffer texture.
Top or bottom rows are missing Width and height do not match the buffer being read Use the matching back-buffer dimensions and verify backend-specific HDPI behavior.
Capture contains the previous frame Capture ran before rendering completed Call the capture after all scene and UI draw calls for that frame.
Out-of-memory or native-memory growth Pixmap objects were not released Wrap the capture in try/finally and call dispose(). Pixmaps use native heap memory; see Pixmaps.
Large framebuffer fails Device or backend cannot support the requested dimensions or attachments Query graphics capabilities, reduce dimensions, or capture in tiles; measure on the target device.
HTML5 screenshot is lower resolution than expected CSS or logical size differs from physical pixels Inspect usePhysicalPixels, then log actual framebuffer dimensions.

9. Performance, reliability, and cost considerations

Reading pixels from the GPU can synchronize the CPU and GPU. Larger buffers also increase memory use, readback time, and PNG encoding time. Capture on demand rather than every frame, and avoid allocating a new large framebuffer for every screenshot. Reuse a framebuffer when the required dimensions stay constant, and dispose it when the screen or resolution changes.

There is no universal quality or speed gain published for one method over another. Measure capture latency, memory, and output quality on the actual backend and device. For automated tests, keep dimensions, clear color, blend state, and camera settings fixed so comparisons are reproducible.

10. Or skip the browser setup

If the screenshot you need is of a website rather than your running libGDX application, ScreenshotNeo provides a one-request screenshot API. It removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and responses identify the page verdict and billing status in headers. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo API documentation for options such as full-page capture, element selectors, dark mode, device presets, retina scale, custom CSS and JavaScript, waits, request blocking, headers, cookies, geolocation, caching, signed links, asynchronous jobs, bulk capture, and usage reporting.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

FAQ

Does saving a PNG at a larger size improve quality?

No. It only interpolates existing pixels. Render at the required resolution first.

Should I use getWidth() or getBackBufferWidth()?

Use the dimensions of the buffer you are reading. Compare both at runtime, especially on high-DPI and HTML5 targets.

Why is my framebuffer image inverted?

Framebuffer textures commonly use OpenGL’s bottom-left origin. Flip once when displaying or encoding.

Can every device use multisample anti-aliasing?

No. Query the graphics capabilities and treat anti-aliasing as backend- and device-dependent.

When should I dispose a Pixmap?

Immediately after writing or otherwise finishing with it. It uses native heap memory.