Стал тут было народ писать игру под андроид и столкнулись в Andengine(кто не знает, это самый популярный граф. 2D движок под андроид) с такой задачей: есть набор соединённых между собой линий, который предствляют собой ландшафт (как сгенерить, можно почитать тут — gameprogrammer.com/fractal.html). Выглядело это примерно так:
Но нам не нужен “мостик”, нам нужна поверхность, да ещё и с текстурой, вобщем чтобы было вот так…
Начали рыть AndEngine, оказалось с текстурами он умеет работать только как со спрайтами, состоящими из двух трианглов. Нас это устроить никак не может, потому что мы заранее не знаем размера ландшафта, а следовательно пропорции UV координат 1:1 нам не канают. Да и впринципе, у нас тут не спрайт, а поверхность и является невыпуклым многогранником. Поэтому нам придётся написать свой велосипед, т.к. гугление не дало нормальных результатов для основной ветки andengine. Хорошо, что у него адекватный интерфейсы классов и всё логичо, стоит только разобраться. Нам нужен свой класс с буффером вершин для трианглов и соответствующими им UV координаты. Сразу скажу, что я не буду вдаваться в объяснение почему не перегружен ряд функций и почему некоторые вещи делаются в опред. местах, andengine — это целый хитросплетёный лес в архитектурном плане и я просто оставлял вещи в той позе, в которых оно работало, ибо перебирать весь мотор движка — на это уйдёт 10 таких статей и пол года жизни.
Поехали…
Сначала мы условимся, что у вас уже есть список, в котором лежат все линии, из которых составлена поверхность. Тот самый «мостик», изображённый на первом скрине.
Начинаем описывать класс, который будет представлять нашу поверхность:
private abstract class GroundShape extends Shape
{
Создадим, для удобства, под каждую вершину объект, который хранит её двухмерные координаты в пространстве и UV.
protected class Vertex
{
float x, y;
float u, v;
};
protected class MorphVertexBuffer extends VertexBuffer
{
public MorphVertexBuffer(int capacity)
{
//Отдаём папе параметры о том, какие мы.
super(capacity, GL11.GL_STATIC_DRAW, true);
}
//Получаем список вершин и укладываем их в буффер праивльно.
public void update(Vertex[] vertexes)
{
int j = 0;
final float[] bufferData = new float[vertexes.length*2];
for (int i = 0; i < vertexes.length; ++i)
{
bufferData[j++] = vertexes[i].x;
bufferData[j++] = vertexes[i].y;
}
final FastFloatBuffer buffer = this.getFloatBuffer();
buffer.position(0);
buffer.put(bufferData);
buffer.position(0);//Обязательно, а то он сам не знает :)
super.setHardwareBufferNeedsUpdate();
}
}
В коде выше описан внутренний класс, который представляет собой буффер с вершинами, который можно скормить движку. Мы наследуемся от VertexBuffer, чтобы оставаться в стандартной архитектуре и описываем метод update(), который заполняет буффер вершин.
Следующим шагом мы создаём тип, который описывает буффер с данными UV координат и метод наложения текстур.
protected class MorphTexture extends BufferObject
{
//ITexture представляет текстуру, которая накладываетя на поверхность.
final ITexture mTexture;
public MorphTexture(ITexture tex, int pCapacity)
{
super(pCapacity, GL11.GL_STATIC_DRAW, true);
mTexture = tex;
}
public void ApplyUV(Vertex [] vertexes)
{
final float[] bufferData = new float[vertexes.length*2];
for (int i = 0, j = 0; i < vertexes.length; ++i)
{
bufferData[j++] = vertexes[i].u;
bufferData[j++] = vertexes[i].v;
}
final FastFloatBuffer buffer = this.getFloatBuffer();
buffer.position(0);
buffer.put(bufferData);
buffer.position(0);//Обязательно, а то он сам не знает :)
super.setHardwareBufferNeedsUpdate();
}
Тут мы опять сформировали буффер, который сможем потом скормить движку.
Далее описывается функци, которая “применяет” текстуру к объекту и выставляет указатель буффера вершин UV координат на тот, что мы сформировали в ApplyUV()
public void onApply(final GL10 pGL) {
this.mTexture.bind(pGL);//Если копнуть в функцию, то это аля glBindTexture()
if(GLHelper.EXTENSIONS_VERTEXBUFFEROBJECTS) {
final GL11 gl11 = (GL11)pGL;
selectOnHardware(gl11);
GLHelper.texCoordZeroPointer(gl11);
} else {
GLHelper.texCoordPointer(pGL, getFloatBuffer());
}
}
}
Далее заводим описанные выше объекты буффера вершин и UV координат.
MorphVertexBuffer m_Buffer;
MorphTexture m_TextureRegion;
int vertexesLimit; //Внутренняя переменная с количеством вершин.
protected BitmapTextureAtlas m_Texture;//Текстура, которую мы будем накладывать
Напоминаю, что описанные выше классы являются внутренними классами GroundShape’а и поэтому дальше мы продолжаем его описание с конструктора, который сам по себе тривиален и нас в нём интересует лишь то, что в него передаётся текстура, которую надо наложить.
public GroundShape(BitmapTextureAtlas texture)
{
super(0, 0);
m_Texture = texture;
}
Далее опсываем функцию инициализации, которая должна быть вызвана в потомке для инициализации буфферов вершин и UV координат.
protected void Init()
{
Vertex[] vertexes = buildVertexBuffer();//этот метод перегружается в потомке.
if (vertexes == null)
return;
//Далее инициализируем объекты.
vertexesLimit = vertexes.length;
m_Buffer = new MorphVertexBuffer(vertexesLimit*2);
m_Buffer.update(vertexes);
m_TextureRegion = new MorphTexture(m_Texture, vertexesLimit*2);
m_TextureRegion.ApplyUV(vertexes);
}
Т.к. GroundShape — абстрактный, мы в потомках обязаны будем перегрузить функцию buildVertexBuffer, в которой нам нужно составить список вершин (с UV координатами) и вернуть их. Вот она
protected abstract Vertex[] buildVertexBuffer();
Следующий шаг — это перегрузка пары методов GroundShape, чтобы рассказать AndEngine что и как рисовать на нашей поверхности.
@Override protected void doDraw(final GL10 pGL, final Camera pCamera)
{
//Применяем текстуру
m_TextureRegion.onApply(pGL);
//Рисуем
super.doDraw(pGL, pCamera);
}
@Override protected void onInitDraw(final GL10 pGL)
{
//Здесь мы говорим что будем рисовать с текстурой и использовать буффер UV координат.
//GLHelper - глобален.
super.onInitDraw(pGL);
GLHelper.enableTextures(pGL);
GLHelper.enableTexCoordArray(pGL);
}
@Override protected void drawVertices(GL10 pGL, Camera arg1)
{
//Рисуем по указанным вершинам.
pGL.glDrawArrays(GL10.GL_TRIANGLES, 0, vertexesLimit);
}
Если вершины UV координат, которые надо использовать мы указывали в doRaw, позвав onApply, то чтобы указать вершины самих треугольников нам не нужно дополнительно звать функции, а просто перегрузить getVertexBuffer и вернуть буффер вершин.
@Override protected VertexBuffer getVertexBuffer()
{
return m_Buffer;
}
Ниже описываются функции, которые просто перегружены по умолчанию и значения для нас никакого не имеют, однако являются обязательной частью в процессе наследования.
@Override public boolean collidesWith(IShape arg0)
{
return false;
}
@Override public float getBaseHeight()
{
return 0;
}
@Override public float getBaseWidth()
{
return 0;
}
@Override public float getHeight()
{
return 0;
}
@Override public float getWidth()
{
return 0;
}
@Override public boolean contains(float arg0, float arg1)
{
return false;
}
@Override protected boolean isCulled(Camera arg0)
{
return false;
}
@Override protected void onUpdateVertexBuffer()
{
}
}
Ок, мы набросали класс, который диктует AndEngine как надо рисовать ЛЮБУЮ “модель”, состоящую из трианглов и имеющую текстуру. Хоть это и 2D движок, он всё равно работает через OpenGL, просто спрайты рисуются на двух треугольниках.
Кстати, обратите внимание, что под андроидом в OGL, нету GL_POLYGONS, лишь GL_TRIANGLES. Самые быстрые из которых, это GL_TRIANGLE_STRIP, читайте о них здесь — en.wikipedia.org/wiki/Triangle_strip. Однако они требуют опред. очерёдности и заморочек, чем заниматься не хотелось, поэтому мы воспользуемся GL_TRIANGLES (учитывая, что при поздних тестах, прирост перформанса был минимален). И так поверхность наша, если смотреть на неё “через” треугольники, должна выглядеть вот так, по сравнению с началом:
Значит теперь нам надо её сгенерировать исходя из списка линий, который нам будут передаваться. Создадим объект для этого:
private class GroundSelf extends GroundShape
{
public GroundSelf(List<Section> sec, BitmapTextureAtlas texture)
{
super(texture);
sections = sec;//Поверхность состоит из гипотетических секций, внутри которых есть линии.
Init();//Зовём ту самую инициализацию GroundShape’а
}
А GroundShape::Init(), как мы помним, будет звать buildVertexBuffer(), который каждый наследник обязан перегружать. В этой функции нам надо построить все вершины каждого треугольника и задать UV координаты. Стоит задуматься, что текстура у нас квадратная, а земля — вобще является невыпуклым многогранником и если мы тупо натянет на все трианглы текстуру в координатной пропорции 1:1, то мы даже текстуры-то как изображения не разберём. Нам нужно уметь задавать множители, причём, т.к. длинна больше высоты, U координаты должны быть по коэффициенту больше.
Я настоятельно рекомендую, когда вы будете работать с текстурами, возьмите в качестве рисунка — компас какой-нибудь, чтобы вы смогли правильно определить ориентацию текстурных координат.
Фактически в buildVertexBuffer функции вы определяете все треугольники вашего объекта и его UV координаты.
@Override protected Vertex[] buildVertexBuffer()
{
int vertexesCount = 0, i, j, k = 0;
float hellY = 800.0f;
final float maxU = 4.0f;//Сколько раз повторить текстуру по U
final float maxV = 2.0f;//и по V
float stepU;
//Получае первые точку первой линии первой секции
//Она - это базовая линия, относительно которой мы ориентируемся.
//И это максимум по V.
float startV = sections.get(0).lines.get(0).line.getY1();
float valueV = hellY - sections.get(0).lines.get(0).line.getY1();
for (i = 0; i < sections.size(); ++i)
vertexesCount += sections.get(i).lines.size()*6;
Vertex[] res = new Vertex[vertexesCount];
Section tmpSection;
Line tmpLine;
for (i = 0; i < sections.size(); ++i)
{
tmpSection = sections.get(i);
//Идём по всем линиям и заполняем наш массив вершин
//Значениями. По два треугольника на линию.
//Или по 6 вершин.
for (j = 0; j < tmpSection.lines.size(); ++j)
{
tmpLine = tmpSection.lines.get(j).line;
stepU = maxU/(float)tmpSection.lines.size();
res[k] = new Vertex();
res[k].x = tmpLine.getX1();
res[k].y = tmpLine.getY1();
res[k].u = (float)j*stepU;
res[k++].v = 0.0f;
res[k] = new Vertex();
res[k].x = tmpLine.getX1();
res[k].y = hellY;
res[k].u = (float)j*stepU;
res[k++].v = maxV + ((startV - tmpLine.getY1())/valueV)*maxV;
res[k] = new Vertex();
res[k].x = tmpLine.getX2();
res[k].y = tmpLine.getY2();
res[k].u = (float)(j + 1)*stepU;
res[k++].v = 0.0f;
res[k] = new Vertex();
res[k].x = tmpLine.getX2();
res[k].y = tmpLine.getY2();
res[k].u = (float)(j + 1)*stepU;
res[k++].v = 0.0f;
res[k] = new Vertex();
res[k].x = tmpLine.getX1();
res[k].y = hellY;
res[k].u = (float)j*stepU;
res[k++].v = maxV + ((startV - tmpLine.getY1())/valueV)*maxV;
res[k] = new Vertex();
res[k].x = tmpLine.getX2();
res[k].y = hellY;
res[k].u = (float)(j + 1)*stepU;
res[k++].v = maxV + ((startV - tmpLine.getY1())/valueV)*maxV;
}
}
//Возвращаем список вершин, который получился.
return res;
}
List<Section> sections;
}
Поверхность создали. Теперь нам надо её прикрепить к миру. Делается это как обычно в AndEngine:
//Создаём объект
grndSelf = new GroundSelf(sections, EvoGlobal.getTextureCache().get(EvoTextureCache.tex_ground).texture);
//Прикрепляем к AndEngine
EvoGlobal.getWorld().getScene().attachChild(grndSelf);
Результат:
Надеюсь тот, кто сейчас пришёл сюда через гугл в поисках решения — удовлетворён.
Автор: Alanir