Всем привет. Хочу поделиться с вами своим «велосипедом», вдруг кому-нибудь еще он пригодится.
(Статья о том как реализовать буфер глубины в Graphics2D).
Проблема
Однажды понадобилось нарисовать карту города с 3D-зданиями. При этом использовать можно было только стандартный Swing`овский Graphics2D. С проецированием полигонов на 2D-плоскость все ясно и тривиально, но для полноценного 3D этого не достаточно, поскольку здания бывают сложные и они могут перекрывать друг друга. В результате становится важен порядок рисования граней здания (передняя грань должна быть нарисована после задней). Казалось бы, можно отсортировать все грани в нужном порядке и все будет идеально. Однако, если грани пересекаются, то сортировка не решает проблему. Пример на рисунке:
Насколько мне известно, данная проблема имеет как минимум два решения: одно из них — использование буфера глубины (z-buffer), другое — разбиение полигонов на мелкие части с целью исключить пересечения.
Решение
Стандартной реализации буфера глубины (насколько мне известно) нет в Java 2D и очень долго я не мог найти решения этой проблемы, пока пристально не всмотрелся в java.awt.Composite. Как оказалось, он почти идеально подходит для реализации буфера глубины и вот как это можно сделать (страничка проекта с исподниками ).
Как вы прекрасно знаете, буфер глубины есть не что иное, как массив z-координат пикселей экрана. Перед началом процедуры рисования он сбрасывается в какое-то значение (например, устанавливается значение самой далекой z-координаты). Затем при выводе каждой новой точки на экран выполняется проверка — меньше ли значение z-координаты выводимой точки, чем значение z-координаты уже хранящейся в буфере глубины. И если это так, то точка будет выведена на экран и значение z-координаты в буфере обновится, иначе точка будет просто отброшена. Таким образом, на экран будет выведена правильная картинка.
Основная идея заключается в том, что мы можем использовать Composite (композит) для реализации буфера глубины, поскольку в процессе рисования каждая точка, выводимая на экран, обрабатывается установленным в графическом контексте композитом. Таким образом, в композите мы можем завести массив координат (размер которого совпадает с размером графического контекста) и проверять в нашем композите каждую выводимую точку. Как видите, звучит просто.
Реализация
Для реализации сказанного выше нам, прежде всего, необходимо научится определять значение z-координаты, для произвольной точки экрана (x, y). Для этого заведем соответствующий интерфейс ZValueResolver:
Перед вызовом какого-либо метода рисования, доступного через Graphics2D (например, drawPolygon), мы будет описывать соответствующий ZValueResolver, который сможет получить z-координату для любой точки (x, y) этого полигона. (Забегая вперед, скажу, что в большинстве случаев реализация тривиальна, поскольку почти всегда ZValueResolver описывается уравнением плоскости по трем точкам, есть даже соответствующая стандаратная реализация).
Далее, все просто: реализуем интерфейс java.awt.Composite, который будет хранить в себе значения буфера глубины:
Таким образом, у нас есть буфер глубины, мы можем определять z координату любой точки (x,y), осталось реализовать только CompositeContext:
Таким образом, записываем поступающий пиксель только в том случае, если он подходит нам по z-координате (srcZ <= dstZ).
Anti-Aliasing
Как ни удивительно, но данная реализация работает только пока мы не пытаемся использовать Anti-Aliasing. Включение сглаживания приводит к непредсказуемым эффектам и артефактам. Это связно с реализаций самого алгоритма сглаживания. Суть проблемы в том, что алгоритм при рисовании периодически выходит за «рамки» рисуемых полигонов, что приводит к неправильному определению z-координат. В связи с этим пришлось добавить проверку выхода токи за полигон в реализации ZValueResolver, что негативно сказалось на производительности, но позволило правильно реализовать буфер глубины в режиме сглаживания.
Вот что получилось в результате (ура! цель достигнута):
Так же простой пример работы с моей реализацией буфера глубины (ну мало ли).
Спасибо за внимание.
(Статья о том как реализовать буфер глубины в Graphics2D).
Проблема
Однажды понадобилось нарисовать карту города с 3D-зданиями. При этом использовать можно было только стандартный Swing`овский Graphics2D. С проецированием полигонов на 2D-плоскость все ясно и тривиально, но для полноценного 3D этого не достаточно, поскольку здания бывают сложные и они могут перекрывать друг друга. В результате становится важен порядок рисования граней здания (передняя грань должна быть нарисована после задней). Казалось бы, можно отсортировать все грани в нужном порядке и все будет идеально. Однако, если грани пересекаются, то сортировка не решает проблему. Пример на рисунке:
Насколько мне известно, данная проблема имеет как минимум два решения: одно из них — использование буфера глубины (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, что негативно сказалось на производительности, но позволило правильно реализовать буфер глубины в режиме сглаживания.
Вот что получилось в результате (ура! цель достигнута):
Так же простой пример работы с моей реализацией буфера глубины (ну мало ли).
Спасибо за внимание.