【问题标题】:Drawing an image using sub-pixel level accuracy using Graphics2D使用 Graphics2D 使用亚像素级精度绘制图像
【发布时间】:2012-01-30 09:44:04
【问题描述】:

我目前正尝试像在视频游戏中一样以固定速率在屏幕上绘制图像。

不幸的是,由于图像移动的速度,一些帧是相同的,因为图像还没有移动一个完整的像素。

有没有办法向 Graphics2D 提供 float 值以在屏幕上位置绘制图像,而不是 int 值?

最初是我所做的:

BufferedImage srcImage = sprite.getImage ( );
Position imagePosition = ... ; //Defined elsewhere
g.drawImage ( srcImage, (int) imagePosition.getX(), (int) imagePosition.getY() );

这当然是阈值,所以图片不会在像素之间移动,而是从一个跳到下一个。

下一个方法是将油漆颜色设置为纹理并在指定位置进行绘制。不幸的是,这会产生不正确的结果,显示平铺而不是正确的抗锯齿。

g.setRenderingHint ( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );

BufferedImage srcImage = sprite.getImage ( );

g.setPaint ( new TexturePaint ( srcImage, new Rectangle2D.Float ( 0, 0, srcImage.getWidth ( ), srcImage.getHeight ( ) ) ) );

AffineTransform xform = new AffineTransform ( );

xform.setToIdentity ( );
xform.translate ( onScreenPos.getX ( ), onScreenPos.getY ( ) );
g.transform ( xform );

g.fillRect(0, 0, srcImage.getWidth(), srcImage.getHeight());

我应该怎么做才能在Java中实现图像的亚像素渲染效果?

【问题讨论】:

    标签: java image graphics graphics2d subpixel


    【解决方案1】:

    在做了类似lawrencealan提议的事情后,我成功解决了我的问题。

    原来我有如下代码,其中g在调用该方法之前转换为16:9坐标系:

    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);
        }
    
    }
    

    但是,正如提问者 Kaushik Shankar 所指出的,将双重位置转换为整数会使图像“跳跃”,而将双重维度转换为整数会使其缩放“跳跃”(为什么 g.drawImage 不接受双打?!)。我发现对我有用的是:

    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);
    
    }
    

    这似乎是一种愚蠢的做法。

    【讨论】:

    • 感谢发帖;我最终也做了类似的事情;很不幸,drawImage 不支持它。
    • 非常感谢!另外,我使用g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 让它看起来更好。
    【解决方案2】:

    您可以使用BufferedImageAffineTransform,绘制到缓冲图像,然后在绘制事件中将缓冲图像绘制到组件。

        /* 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);
        }
    

    在这里查看我的完整示例:http://pastebin.com/hSAkYWqM

    【讨论】:

    • 嘿劳伦!感谢您的回复。非常感谢您向我展示了如何做到这一点!
    • 为什么 t.scale(1, 1); ?? AFAIK 那条指令什么也没做? (缩放到它的实际大小?)
    【解决方案3】:

    您可以使用亚像素精度自行合成图像,但您需要做更多的工作。简单的双线性插值对于游戏来说应该足够好。以下是用于执行此操作的伪 C++ 代码。

    通常,要在 (a,b) 位置绘制精灵,您需要执行以下操作:

    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
    }
    

    要进行亚像素渲染,请执行相同的循环,但要像这样考虑亚像素偏移:

    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
    }
    

    bilinearInterp() 函数看起来像这样:

    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;
    }
    

    这应该不会使用额外的内存,但会花费额外的时间来渲染。

    【讨论】:

    • 我将如何在 java 中进行此类操作?是否可以进行优化?我有很多对象,计算这样的东西似乎需要一些时间......
    • 我对java不是很流利,但大概你的精灵有一些结构,对吧?在基于 C 的语言中,通常有一个指向第一个像素的指针和每行的字节数。我想在 Java 中你可能有一个像素数组,行之间没有间隙。所以一个精灵实际上可能只是一个非常大的红色、绿色、蓝色、alpha 数组(或者可能是 alpha、红色、绿色、蓝色)。因此,无需计算像素结构的地址,您只需获得所需的偏移量。在 bilinearInterp 中,前 2 行只是计算所需点周围 4 个像素的索引。
    • 没错,但在我的代码中计算每一帧可能不是最好的方法。我希望可能有某种内置函数作为执行此操作的 Java API 的一部分。
    【解决方案4】:

    相应地更改图像的分辨率,没有像具有亚像素坐标的位图这样的东西,所以基本上你可以做的是在内存中创建一个比你想要呈现到屏幕上的图像更大的图像,但允许你“亚像素”精度。

    当您绘制到内存中较大的图像时,您会将其复制并重新采样到最终用户可见的较小渲染中。

    例如:一张 100x100 的图片,它是 50x50 调整大小/重新采样的对应物:

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

    【讨论】:

    • 这也实现了双缓冲区,因此您可以对内存中的图像进行多次修改,同时不更新处理器密集型的 UI
    • 想象一个球在屏幕上移动,但它的移动速度很慢。它每秒移动 1 个像素。如果我每秒在屏幕上绘制 2 次,那么第二次,绘制将与第一次完全相同。第三次将显示一步移动一个像素的球。相反,我想要的是让球部分移动,以便可以像在两个像素之间一样绘制它。这个功能在 java 中可用吗?
    • 您必须使用上述过程来模拟它。所以说你的渲染区域是 800x600,你需要一个内存中的图像,它是 1600x1200 的 2 倍——而不是在浮点级别移动,而是在整个像素上移动……所以不是移动一个球 0.5 像素你在更大的基于内存的图像中移动它 1px,当你将它重新采样到可视区域 (800x600) 时,它将被模拟亚像素精度..
    • 哦!我明白你的意思了。内存效率高吗?
    • 必须有其他方法来获得抗锯齿效果,而不必将每个维度加倍!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-23
    • 2013-04-29
    相关资源
    最近更新 更多