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

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

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

Спасибо за внимание.

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s