When down scaling 2D images in Java2D, it does a great job at preserving hard edges while downscaling to non power of 2s. However, in OpenGL, I have been unable to find a solution to this. I have tried using GL_NEAREST to get hard edges, but it also creates the wonky edges aswell. I cannot use GL_LINEAR in this case either, but still, the linear interpolation still looks wonky.

Example (Both rendering 30x30 hard image, rendering at 30x30 pixels).

enter image description here

Please note : I am aware that this occurs with images that aren't power of twos, like 30, but the main problem is, if I try to upscale or downscale an image that is power of two (say 16x16) to something that isn't a power of two (say 20x20), I still get this effect. This issue with scaling this occurs in Java2D, but it is by far less noticable.

My question is: What parameters will I need to use when rendering a non power of two image? So I can get a similar effect that I get in Java 2D.

I can post source code if needed.


Original Image (30 x 30 png)

enter image description here

Turns out I wasn't scaling them at all! Both were being rendered at the original 30 x 30 pixels on both OpenGL and Java2D.

Sample Code: Java2D:

 g.drawImage(heart, 0, 0, 30, 30, null);

No rendering hints are being applied, just the bare minimum.

In OpenGL

Vertex Shader:

#version 400

in vec2 position;

out vec2 textureCoords;

uniform float width;
uniform float height;
uniform float xOffset;
uniform float yOffset;
uniform mat4 transformationMatrix;

void main(void)

    gl_Position = transformationMatrix * vec4(position.x, position.y, 0.0, 1.0);
    gl_Position.x += (xOffset) / (width / 2);
    gl_Position.y += (yOffset) / (height / 2);
    gl_Position.y = gl_Position.y;

    textureCoords = vec2(position.x / 1, position.y / 1);

Fragment Shader:

#version 400

in vec2 textureCoords;

out vec4 out_Color;

uniform sampler2D texture2d;
uniform float transparency;

void main()
    out_Color = texture2D(texture2d, vec2(textureCoords.x, textureCoords.y));
    if(textureCoords.y > 1 || textureCoords.y < 0)

Rendering methods:

public void makeWonkyImage()
    render(0, 0, 30, 30, heart);
public void render(float x, float y, float width, float height, int textureID)
    PostProcess.imageRenderer.render(x, Gfx.HEIGHT - y, width / 1000, -height / 1000, textureID);
public void render(float x, float y, float width, float height, int textureID)

        Vector2f position = new Vector2f((-Display.WIDTH / (width * 1000f))
                , (-Display.HEIGHT / (height * 1000f)));
        Matrix4f matrix = Maths.createTransformationMatrix(position,
                new Vector2f(width / Gfx.WIDTH * 1000f, height / Gfx.HEIGHT * 1000f));
        shader.loadScreenDimensions((float) Display.getWidth(), (float) Display.getHeight());
        shader.loadOffsets(x, y);
        GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, quad.getVertexCount());

public void prepare(int texture)
        GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);

        GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture);
        GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
        GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
  • \$\begingroup\$ This is a somewhat complicated problem in general, as outlined here: What makes scaling pixel art different than other images?. Since the JAVA 2D case came out cleanly though, there may be a particular solution for this specific example. Can you share with us the source image and the scale you're reducing it to? That may help us identify where Java 2D is making different choices (and why), allowing us to duplicate that logic. \$\endgroup\$ – DMGregory Apr 24 '17 at 0:27
  • \$\begingroup\$ @DMGregory Updated, also thanks for linked article, will give it a read. \$\endgroup\$ – Jason Apr 24 '17 at 0:43
  • \$\begingroup\$ If you're not deliberately scaling the image down, then there could be a bug or oversight where a scale is being applied inadvertently (as outlined in the linked answer in the section about runtime scaling based on display size). Showing a minimal sample of code required to reproduce the issue might help in tracking this down. \$\endgroup\$ – DMGregory Apr 24 '17 at 0:46
  • \$\begingroup\$ @DMGregory I've added source code. I feel like you may be right when talking about a problem with the scaling, so I'm starting to feel like its a problem when converting OpenGL space to pixel space. \$\endgroup\$ – Jason Apr 24 '17 at 0:59

TL;DR: There is no guarantee of pixel-perfectness in OpenGL, but try adding (0.375f, 0.375f) of a pixel to your render coordinates. This magic offset is "expected" by many drivers to render pixel-perfect.

There are multiple gotchas when rendering pixel-perfect images in OpenGL (and DirectX, and almost any other GPU APIs in general).

  • Coordinates are given to the GPU in normalized device space (NDS) (-1 to 1) then converted to pixels before drawing. To render in pixel coordinates we use a model-view-projection matrix which cancels out the normalized device space. This process (pixels->NDS->pixels) can create rounding errors.
  • Not all GPUs can render non-power-of-two (NPOT) textures the same way. Some can't at all in hardware and the texture will be scaled to a power-of-two by the driver. Even if the GPU can, rounding errors may occur. It is best to expand your texture to the next power of two and crop it using the texture coordinates.
  • Screen rotation in some drivers can add a 1 pixel offset causing the geometry to not line up with the edge(s) of the screen.
  • Anti-aliasing, multi-sampling, super-sampling, etc, may ruin your pixel-perfect texture sampling and rendering.
  • Drivers sometimes add a magic fraction-of-a-pixel offset to the screen-space coordinates and/or a magic fraction-of-a-texel offset to the texture coordinates.

Regarding that last point, try adding (0.375f, 0.375f) of a pixel to all your drawing coordinates. It may be all that's needed on "most common" OpenGL implementations to get pixel-perfect rendering. Note that screen rotation may require negating the X,Y or both values of this offset.

If you search the web for "opengl" and "0.375" you will find many different explanations and opinions as to why this magic offset exists.

  • \$\begingroup\$ Turns out it was a rounding error with my positions. Thanks for this info! \$\endgroup\$ – Jason Apr 24 '17 at 22:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.