Drawing an image using sub-pixel level accuracy using Graphics2D

前端 未结 4 1233
北荒
北荒 2021-02-15 14:48

I am currently attempting to draw images on the screen at a regular rate like in a video game.

Unfortunately, because of the rate at which the image is moving, some fram

相关标签:
4条回答
  • 2021-02-15 15:22

    You can composite the image yourself using sub-pixel accuracy, but it's more work on your part. Simple bilinear interpolation should work well enough for a game. Below is psuedo-C++ code for doing it.

    Normally, to draw a sprite at location (a,b), you'd do something like this:

    for (x = a; x < a + sprite.width; x++)
    {
        for (y = b; y < b + sprite.height; y++)
        {
            *dstPixel = alphaBlend (*dstPixel, *spritePixel);
            dstPixel++;
            spritePixel++;
        }
        dstPixel += destLineDiff; // Move to start of next destination line
        spritePixel += spriteLineDiff; // Move to start of next sprite line
    }
    

    To do sub-pixel rendering, you do the same loop, but account for the sub-pixel offset like so:

    float xOffset = a - floor (a);
    float yOffset = b - floor (b);
    for (x = floor(a), spriteX = 0; x < floor(a) + sprite.width + 1; x++, spriteX++)
    {
        for (y = floor(b), spriteY = 0; y < floor (b) + sprite.height + 1; y++, spriteY++)
        {
            spriteInterp = bilinearInterp (sprite, spriteX + xOffset, spriteY + yOffset);
            *dstPixel = alphaBlend (*dstPixel, spriteInterp);
            dstPixel++;
            spritePixel++;
        }
        dstPixel += destLineDiff; // Move to start of next destination line
        spritePixel += spriteLineDiff; // Move to start of next sprite line
    }
    

    The bilinearInterp() function would look something like this:

    Pixel bilinearInterp (Sprite* sprite, float x, float y)
    {
        // Interpolate the upper row of pixels
        Pixel* topPtr = sprite->dataPtr + ((floor (y) + 1) * sprite->rowBytes) + floor(x) * sizeof (Pixel);
        Pixel* bottomPtr = sprite->dataPtr + (floor (y) * sprite->rowBytes) + floor (x) * sizeof (Pixel);
    
        float xOffset = x - floor (x);
        float yOffset = y - floor (y);
    
        Pixel top = *topPtr + ((*(topPtr + 1) - *topPtr) * xOffset;
        Pixel bottom = *bottomPtr + ((*(bottomPtr + 1) - *bottomPtr) * xOffset;
        return bottom + (top - bottom) * yOffset;
    }
    

    This should use no additional memory, but will take additional time to render.

    0 讨论(0)
  • 2021-02-15 15:27

    Change the resolution of your image accordingly, there's no such thing as a bitmap with sub-pixel coordinates, so basically what you can do is create an in memory image larger than what you want rendered to the screen, but allows you "sub-pixel" accuracy.

    When you draw to the larger image in memory, you copy and resample that into the smaller render visible to the end user.

    For example: a 100x100 image and it's 50x50 resized / resampled counterpart:

    resampling

    See: http://en.wikipedia.org/wiki/Resampling_%28bitmap%29

    0 讨论(0)
  • 2021-02-15 15:28

    You can use a BufferedImage and AffineTransform, draw to the buffered image, then draw the buffered image to the component in the paint event.

        /* overrides the paint method */
        @Override
        public void paint(Graphics g) {
            /* clear scene buffer */
            g2d.clearRect(0, 0, (int)width, (int)height);
    
            /* draw ball image to the memory image with transformed x/y double values */
            AffineTransform t = new AffineTransform();
            t.translate(ball.x, ball.y); // x/y set here, ball.x/y = double, ie: 10.33
            t.scale(1, 1); // scale = 1 
            g2d.drawImage(image, t, null);
    
            // draw the scene (double percision image) to the ui component
            g.drawImage(scene, 0, 0, this);
        }
    

    Check my full example here: http://pastebin.com/hSAkYWqM

    0 讨论(0)
  • 2021-02-15 15:28

    I successfully solved my problem after doing something like lawrencealan proposed.

    Originally, I had the following code, where g is transformed to a 16:9 coordinate system before the method is called:

    private void drawStar(Graphics2D g, Star s) {
    
        double radius = s.getRadius();
        double x = s.getX() - radius;
        double y = s.getY() - radius;
        double width = radius*2;
        double height = radius*2;
    
        try {
    
            BufferedImage image = ImageIO.read(this.getClass().getResource("/images/star.png"));
            g.drawImage(image, (int)x, (int)y, (int)width, (int)height, this);
    
        } catch (IOException ex) {
            Logger.getLogger(View.class.getName()).log(Level.SEVERE, null, ex);
        }
    
    }
    

    However, as noted by the questioner Kaushik Shankar, turning the double positions into integers makes the image "jump" around, and turning the double dimensions into integers makes it scale "jumpy" (why the hell does g.drawImage not accept doubles?!). What I found working for me was the following:

    private void drawStar(Graphics2D g, Star s) {
    
        AffineTransform originalTransform = g.getTransform();
    
        double radius = s.getRadius();
        double x = s.getX() - radius;
        double y = s.getY() - radius;
        double width = radius*2;
        double height = radius*2;
    
        try {
    
            BufferedImage image = ImageIO.read(this.getClass().getResource("/images/star.png"));
    
            g.translate(x, y);
            g.scale(width/image.getWidth(), height/image.getHeight());
    
            g.drawImage(image, 0, 0, this);
    
        } catch (IOException ex) {
            Logger.getLogger(View.class.getName()).log(Level.SEVERE, null, ex);
        }
    
        g.setTransform(originalTransform);
    
    }
    

    Seems like a stupid way of doing it though.

    0 讨论(0)
提交回复
热议问题