镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

[rtextures] Fix ImageRotate pixel-center sampling - #6356

Open
mmporong wants to merge 1 commit into
raysan5:masterfrom
mmporong:fix/image-rotate-pixel-centers
Open

mmporong wants to merge 1 commit into
raysan5:masterfrom
mmporong:fix/image-rotate-pixel-centers

Conversation

@mmporong

@mmporong mmporong commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

ImageRotate() currently maps destination pixel corners back into the source image. At right angles this shifts the sampled pixels, sends edge coordinates outside the image, and leaves transparent pixels in the output. For example, rotating a 2x3 RGBA image by 180 degrees leaves four of its six pixels transparent.

This change maps destination pixel centers to source pixel centers. It accepts coordinates within the source pixels' half-pixel extent, clamps interpolation at the image edge, and rounds interpolated channel values to the nearest integer.

The channel rounding is needed to recover exact source values at right angles when floating-point trigonometry produces values just below an integer. For non-right-angle interpolation, this can change a channel by at most one compared with the previous truncation.

I tested the change with distinct per-pixel RGBA values and hard-coded expected outputs for 2x3 and 3x3 images. The cases cover 0, 90, 180, 270, and -90 degrees, plus 360 degrees as an out-of-contract diagnostic. All cases preserve the expected dimensions and contain no unexpected transparent pixels.

Validation performed:

  • Linux CMake Release static library build
  • Regression executable linked against the built libraylib.a: 9/9 cases passed
  • Linux CMake Debug static library build with AddressSanitizer and UndefinedBehaviorSanitizer: 9/9 cases passed with no sanitizer errors

The following headless reproducer links against the built static library. It rotates six unique RGBA8 pixels by 180 degrees and checks the complete output, including alpha:

#include "raylib.h"

#include <assert.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    static const Color source[] = {
        { 1, 11, 21, 31 }, { 2, 12, 22, 32 },
        { 3, 13, 23, 33 }, { 4, 14, 24, 34 },
        { 5, 15, 25, 35 }, { 6, 16, 26, 36 }
    };
    static const Color expected[] = {
        { 6, 16, 26, 36 }, { 5, 15, 25, 35 },
        { 4, 14, 24, 34 }, { 3, 13, 23, 33 },
        { 2, 12, 22, 32 }, { 1, 11, 21, 31 }
    };
    Image image = { malloc(sizeof(source)), 2, 3, 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 };

    assert(image.data != NULL);
    memcpy(image.data, source, sizeof(source));
    ImageRotate(&image, 180);

    assert(image.width == 2);
    assert(image.height == 3);
    assert(memcmp(image.data, expected, sizeof(expected)) == 0);

    UnloadImage(image);
    return 0;
}

I compiled it with a C99 compiler, the raylib src include directory, and the CMake Release libraylib.a. It exits with status 0 and does not initialize a window or graphics device.

I did not run the Windows, macOS, WebAssembly, or Android builds. I also did not run GUI examples or GPU rendering tests; this change is in the CPU image manipulation path.

@raysan5

raysan5 commented Nov 4, 2026

Copy link
Copy Markdown
Owner

@mmporong please, could you post some screenshots with before-after? not sure if I understand the issue it addresses...

@mmporong

mmporong commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Sure, here are before/after images.

ImageRotate() maps output pixel corners instead of pixel centers back into the source image, so the rotated result is shifted. At 90, 180 and 270 degrees this leaves a transparent row or column in the output and drops one border row or column of the source; at other angles the result ends up about half a pixel off center.

Test image: 8x5 pixels with a 1-pixel border in a different color on each side (blue top, green right, orange bottom, red left). All images are scaled 24x with ImageResizeNN(), gray lines show the pixel grid and the checkerboard shows transparent pixels.

source image

90 degrees. Left: master, middle: this PR, right: ImageRotateCW() as reference. On master the image is one pixel to the right, the left column is transparent and the blue top border is gone.

ImageRotate 90 degrees: master, this PR, ImageRotateCW

180 degrees. Left: master, middle: this PR, right: ImageRotateCW() twice as reference. On master the image is one pixel to the right and down, the top row and left column are transparent and the blue and red borders are gone.

ImageRotate 180 degrees: master, this PR, ImageRotateCW twice

45 degrees. Left: master, right: this PR. On master the shape is half a pixel off center, so the blue and red borders are blended with the dark fill while the orange and green ones keep their full color. With this PR all four borders keep their color.

ImageRotate 45 degrees: master, this PR

Numeric check with an 8x5 image that has a different color in every pixel: at 90 degrees master has 8 transparent pixels, 40 of 40 pixels differ from ImageRotateCW() and 8 of the 40 source colors are missing, while this PR matches ImageRotateCW() exactly (0 of 40). This PR also matches ImageRotateCW()/ImageRotateCCW() exactly at 180, 270 and -90 degrees (master: 12, 5 and 6 transparent pixels), and at 45 degrees the center of the opaque area is 0.50 px right and 0.50 px down of the image center on master and exactly centered (0.00, 0.00) with this PR.

Master is 3b12ba1 and this PR is e8f2398. The harness uses only CPU image functions (no window), and its source and the 1:1 PNGs are here: https://github.com/mmporong/raylib/tree/pr-6356-screenshots/pr-6356

@raysan5

raysan5 commented Nov 4, 2026

Copy link
Copy Markdown
Owner

@mmporong is rotation following other drawing functions rotation convention?

@mmporong

mmporong commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Yes. This PR does not change the direction, only where the samples are taken (pixel centers instead of pixel corners). The sin/cos terms are the same as on master.

ImageRotate() maps each output pixel back into the source with oldX = X*cos + Y*sin and oldY = Y*cos - X*sin (X, Y relative to the center). That is the inverse of x*cos - y*sin, x*sin + y*cos, the rotation that DrawTexturePro(), DrawRectanglePro() and ImageDrawRectanglePro() apply to their corners. With y pointing down, a positive angle turns the image clockwise on screen in all of them, so ImageRotate(&image, 90) gives the same pixels as ImageRotateCW() (right panel of the 90 degree image above), and -90 gives the same pixels as ImageRotateCCW().

@raysan5

raysan5 commented Nov 5, 2026

Copy link
Copy Markdown
Owner

@mmporong sorry, I don't understand your answer if yes or no. please, could you reread my question and be more clear? provide 45deg rotation samples with the other functions using same image.

@mmporong

mmporong commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Yes. ImageRotate() follows the same convention as the drawing functions: a positive angle rotates clockwise on screen.

Here is the same image rotated 45 degrees with each function:

Source image, then the same image rotated 45 degrees with ImageRotate, DrawTexturePro and DrawTextureEx

In all three results the blue top border ends up on the upper right.

I also compared the pixels. Against ImageRotate() from this PR, DrawTexturePro() and DrawTextureEx() differ in 18 of 22900 pixels, by 1/255 at most. Against ImageRotate() from master they differ in 2196 pixels, by up to 115/255, because of the half-pixel offset this PR fixes.

Setup: 192x120 image, bilinear filter, 300x300 canvas, all three rotated around the canvas center, drawn with Mesa software OpenGL. DrawTextureEx() pivots on the top-left corner, so I moved its position to keep the same center. Test program: rotate_convention.c.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants