Стал тут было народ писать игру под андроид и столкнулись в Andengine(кто не знает, это самый популярный граф. 2D движок под андроид) с такой задачей: есть набор соединённых между собой линий, который предствляют собой ландшафт (как сгенерить, можно почитать тут — gameprogrammer.com/fractal.html). Выглядело это примерно так:

Но нам не нужен “мостик”, нам нужна поверхность, да ещё и с текстурой, вообщем чтобы было вот так…

Начали рыть AndEngine, оказалось с текстурами он умеет работать только как со спрайтами, состоящими из двух трианглов. Нас это устроить никак не может, потому что мы заранее не знаем размера ландшафта, а следовательно пропорции UV координат 1:1 нам не канают. Да и в принципе, у нас тут не спрайт, а поверхность и является невыпуклым многогранником. Поэтому нам придётся написать свой велосипед, т.к. гугление не дало нормальных результатов для основной ветки andengine. Хорошо, что у него адекватный интерфейсы классов и всё логично, стоит только разобраться. Нам нужен свой класс с буфером вершин для трианглов и соответствующими им UV координаты. Сразу скажу, что я не буду вдаваться в объяснение почему не перегружен ряд функций и почему некоторые вещи делаются в опред. местах, andengine — это целый хитросплетённый лес в архитектурном плане и я просто оставлял вещи в той позе, в которых оно работало, ибо перебирать весь мотор движка — на это уйдёт 10 таких статей и пол года жизни.
Поехали…
Сначала мы условимся, что у вас уже есть список, в котором лежат все линии, из которых составлена поверхность. Тот самый «мостик», изображённый на первом скрине.
Начинаем описывать класс, который будет представлять нашу поверхность:
Создадим, для удобства, под каждую вершину объект, который хранит её двухмерные координаты в пространстве и UV.
В коде выше описан внутренний класс, который представляет собой буфер с вершинами, который можно скормить движку. Мы наследуемся от VertexBuffer, чтобы оставаться в стандартной архитектуре и описываем метод update(), который заполняет буфер вершин.
Следующим шагом мы создаём тип, который описывает буфер с данными UV координат и метод наложения текстур.
Тут мы опять сформировали буфер, который сможем потом скормить движку.
Далее описывается функция, которая “применяет” текстуру к объекту и выставляет указатель буфера вершин UV координат на тот, что мы сформировали в ApplyUV()
Далее заводим описанные выше объекты буфера вершин и UV координат.
Напоминаю, что описанные выше классы являются внутренними классами GroundShape’а и поэтому дальше мы продолжаем его описание с конструктора, который сам по себе тривиален и нас в нём интересует лишь то, что в него передаётся текстура, которую надо наложить.
Далее описываем функцию инициализации, которая должна быть вызвана в потомке для инициализации буферов вершин и UV координат.
Т.к. GroundShape — абстрактный, мы в потомках обязаны будем перегрузить функцию buildVertexBuffer, в которой нам нужно составить список вершин (с UV координатами) и вернуть их. Вот она
Следующий шаг — это перегрузка пары методов GroundShape, чтобы рассказать AndEngine что и как рисовать на нашей поверхности.
Если вершины UV координат, которые надо использовать мы указывали в doRaw, позвав onApply, то чтобы указать вершины самих треугольников нам не нужно дополнительно звать функции, а просто перегрузить getVertexBuffer и вернуть буфер вершин.
Ниже описываются функции, которые просто перегружены по умолчанию и значения для нас никакого не имеют, однако являются обязательной частью в процессе наследования.
Ок, мы набросали класс, который диктует AndEngine как надо рисовать ЛЮБУЮ “модель”, состоящую из трианглов и имеющую текстуру. Хоть это и 2D движок, он всё равно работает через OpenGL, просто спрайты рисуются на двух треугольниках.
Кстати, обратите внимание, что под андроидом в OGL, нету GL_POLYGONS, лишь GL_TRIANGLES. Самые быстрые из которых, это GL_TRIANGLE_STRIP, читайте о них здесь — en.wikipedia.org/wiki/Triangle_strip. Однако они требуют определённой очерёдности и заморочек, чем заниматься не хотелось, поэтому мы воспользуемся GL_TRIANGLES (учитывая, что при поздних тестах, прирост перформанса был минимален). И так поверхность наша, если смотреть на неё “через” треугольники, должна выглядеть вот так, по сравнению с началом:

Значит теперь нам надо её сгенерировать исходя из списка линий, который нам будут передаваться. Создадим объект для этого:
А GroundShape::Init(), как мы помним, будет звать buildVertexBuffer(), который каждый наследник обязан перегружать. В этой функции нам надо построить все вершины каждого треугольника и задать UV координаты. Стоит задуматься, что текстура у нас квадратная, а земля — вообще является невыпуклым многогранником и если мы тупо натянет на все трианглы текстуру в координатной пропорции 1:1, то мы даже текстуры-то как изображения не разберём. Нам нужно уметь задавать множители, причём, т.к. длина больше высоты, U координаты должны быть по коэффициенту больше.
Я настоятельно рекомендую, когда вы будете работать с текстурами, возьмите в качестве рисунка — компас какой-нибудь, чтобы вы смогли правильно определить ориентацию текстурных координат.
Фактически в buildVertexBuffer функции вы определяете все треугольники вашего объекта и его UV координаты.
Поверхность создали. Теперь нам надо её прикрепить к миру. Делается это как обычно в AndEngine:
Результат:

Надеюсь тот, кто сейчас пришёл сюда через гугл в поисках решения — удовлетворён.

Но нам не нужен “мостик”, нам нужна поверхность, да ещё и с текстурой, вообщем чтобы было вот так…

Начали рыть 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 = maxV + ((startV - tmpLine.getY1())/valueV)*maxV; res[k] = new Vertex(); res[k].x = tmpLine.getX1(); res[k].y = hellY; res[k].u = (float)j*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 = maxV + ((startV - tmpLine.getY2())/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 = maxV + ((startV - tmpLine.getY2())/valueV)*maxV; res[k] = new Vertex(); res[k].x = tmpLine.getX1(); res[k].y = hellY; res[k].u = (float)j*stepU; res[k++].v = 0.0f; res[k] = new Vertex(); res[k].x = tmpLine.getX2(); res[k].y = hellY; res[k].u = (float)(j + 1)*stepU; res[k++].v = 0.0f; } } //Возвращаем список вершин, который получился. return res; } List<Section> sections; }
Поверхность создали. Теперь нам надо её прикрепить к миру. Делается это как обычно в AndEngine:
//Создаём объект grndSelf = new GroundSelf(sections, EvoGlobal.getTextureCache().get(EvoTextureCache.tex_ground).texture); //Прикрепляем к AndEngine EvoGlobal.getWorld().getScene().attachChild(grndSelf);
Результат:

Надеюсь тот, кто сейчас пришёл сюда через гугл в поисках решения — удовлетворён.
