Pull to refresh

Java 2D и буфер глубины

Reading time7 min
Views3.8K
imageВсем привет. Хочу поделиться с вами своим «велосипедом», вдруг кому-нибудь еще он пригодится.

(Статья о том как реализовать буфер глубины в Graphics2D).

 



Проблема
Однажды понадобилось нарисовать карту города с 3D-зданиями. При этом использовать можно было только стандартный Swing`овский Graphics2D. С проецированием полигонов на 2D-плоскость все ясно и тривиально, но для полноценного 3D этого не достаточно, поскольку здания бывают сложные и они могут перекрывать друг друга. В результате становится важен порядок рисования граней здания (передняя грань должна быть нарисована после задней). Казалось бы, можно отсортировать все грани в нужном порядке и все будет идеально. Однако, если грани пересекаются, то сортировка не решает проблему. Пример на рисунке:

image

Насколько мне известно, данная проблема имеет как минимум два решения: одно из них — использование буфера глубины (z-buffer), другое — разбиение полигонов на мелкие части с целью исключить пересечения.

Решение
Стандартной реализации буфера глубины (насколько мне известно) нет в Java 2D и очень долго я не мог найти решения этой проблемы, пока пристально не всмотрелся в java.awt.Composite. Как оказалось, он почти идеально подходит для реализации буфера глубины и вот как это можно сделать (страничка проекта с исподниками ).

Как вы прекрасно знаете, буфер глубины есть не что иное, как массив z-координат пикселей экрана. Перед началом процедуры рисования он сбрасывается в какое-то значение (например, устанавливается значение самой далекой z-координаты). Затем при выводе каждой новой точки на экран выполняется проверка — меньше ли значение z-координаты выводимой точки, чем значение z-координаты уже хранящейся в буфере глубины. И если это так, то точка будет выведена на экран и значение z-координаты в буфере обновится, иначе точка будет просто отброшена. Таким образом, на экран будет выведена правильная картинка.

Основная идея заключается в том, что мы можем использовать Composite (композит) для реализации буфера глубины, поскольку в процессе рисования каждая точка, выводимая на экран, обрабатывается установленным в графическом контексте композитом. Таким образом, в композите мы можем завести массив координат (размер которого совпадает с размером графического контекста) и проверять в нашем композите каждую выводимую точку. Как видите, звучит просто.

Реализация
Для реализации сказанного выше нам, прежде всего, необходимо научится определять значение z-координаты, для произвольной точки экрана (x, y). Для этого заведем соответствующий интерфейс ZValueResolver:

/**
* Converts given x, y coordinate to z coordinate
* @author caiiiycuk
*
*/
public interface ZValueResolver {

    /**
     * @param x given x coordinate
     * @param y given y coordinate
     * @return z coordinate of x, y
     */
    double resolve(double x, double y);
    
  ...
}


Перед вызовом какого-либо метода рисования, доступного через Graphics2D (например, drawPolygon), мы будет описывать соответствующий ZValueResolver, который сможет получить z-координату для любой точки (x, y) этого полигона. (Забегая вперед, скажу, что в большинстве случаев реализация тривиальна, поскольку почти всегда ZValueResolver описывается уравнением плоскости по трем точкам, есть даже соответствующая стандаратная реализация).

Далее, все просто: реализуем интерфейс java.awt.Composite, который будет хранить в себе значения буфера глубины:

/**
* ZComposite emulates ZBuffer
* @author caiiiycuk
*/
public class ZComposite implements Composite {

  ...

    protected double[] buffer;
    protected int width;
    protected int height;

  ...      
    
    /**
     * Set z-value in buffer for given point
     * @param x coordinate
     * @param y coordinate
     * @param value z-value
     */
    public void setZOf(int x, int y, double value) {
        if (x >= width || x < 0 ||
            y >= height || y < 0) {
            throw new IllegalArgumentException("Point [" + x + ", " + y + "] is outside of the Z Buffer array");
        }
        
        buffer[y*width + x] = value;
    }
    
    public double getZOf(int realX, int realY) {
        return buffer[realY*width + realX];
    }

  ...
    
}


Таким образом, у нас есть буфер глубины, мы можем определять z координату любой точки (x,y), осталось реализовать только CompositeContext:

/**
* Composite emulates Z buffer
* @author caiiiycuk
*/
public class ZCompositeContext implements CompositeContext {

    protected final static byte R_BAND = 0;
    protected final static byte G_BAND = 1;
    protected final static byte B_BAND = 2;
    
    protected ZComposite zComposite;
    
    ZCompositeContext(ZComposite zComposite) {
        this.zComposite = zComposite;
    }

    /**
     * {@inheritDoc}
     */
    public void compose(Raster src, Raster dstIn, WritableRaster dstOut) {
        ZValueResolver zValueResolver = zComposite.getValueResolver();
        
        if (zValueResolver == null) {
            throw new IllegalArgumentException("You must set a ZValueResolver before draw any polygon with this composite");
        }
        
        int maxX = dstOut.getMinX() + dstOut.getWidth();
        int maxY = dstOut.getMinY() + dstOut.getHeight();

        for (int y = dstOut.getMinY(); y < maxY; y++) {
            for (int x = dstOut.getMinX(); x < maxX; x++) {
                int dstInX = -dstIn.getSampleModelTranslateX() + x;
                int dstInY = -dstIn.getSampleModelTranslateY() + y;
                
                double dstZ = zComposite.getZOf(dstInX, dstInY);
                double srcZ = zValueResolver.resolve(dstInX, dstInY);

                if (srcZ < dstZ) {
                    zComposite.setZOf(dstInX, dstInY, srcZ);
                    dstOut.setSample(x, y, R_BAND, src.getSample(x, y, R_BAND)); //R
                    dstOut.setSample(x, y, G_BAND, src.getSample(x, y, G_BAND)); //G
                    dstOut.setSample(x, y, B_BAND, src.getSample(x, y, B_BAND)); //B
                } else if (srcZ == dstZ) {
                    dstOut.setSample(x, y, R_BAND, src.getSample(x, y, R_BAND)); //R
                    dstOut.setSample(x, y, G_BAND, src.getSample(x, y, G_BAND)); //G
                    dstOut.setSample(x, y, B_BAND, src.getSample(x, y, B_BAND)); //B
                } else {
                    dstOut.setSample(x, y, R_BAND, dstIn.getSample(x, y, R_BAND)); //R
                    dstOut.setSample(x, y, G_BAND, dstIn.getSample(x, y, G_BAND)); //G
                    dstOut.setSample(x, y, B_BAND, dstIn.getSample(x, y, B_BAND)); //B
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void dispose() {
    }

}


Таким образом, записываем поступающий пиксель только в том случае, если он подходит нам по z-координате (srcZ <= dstZ).

Anti-Aliasing
Как ни удивительно, но данная реализация работает только пока мы не пытаемся использовать Anti-Aliasing. Включение сглаживания приводит к непредсказуемым эффектам и артефактам. Это связно с реализаций самого алгоритма сглаживания. Суть проблемы в том, что алгоритм при рисовании периодически выходит за «рамки» рисуемых полигонов, что приводит к неправильному определению z-координат. В связи с этим пришлось добавить проверку выхода токи за полигон в реализации ZValueResolver, что негативно сказалось на производительности, но позволило правильно реализовать буфер глубины в режиме сглаживания.

Вот что получилось в результате (ура! цель достигнута):

image

Так же простой пример работы с моей реализацией буфера глубины (ну мало ли).

Спасибо за внимание.
Tags:
Hubs:
+21
Comments3

Articles

Change theme settings