Andengine: произвольный ландшафт с текстурой

в 12:54, , рубрики: AndEngine, android, game development, java, Разработка под android, метки: , , ,

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

image

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

image

Начали рыть 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 (учитывая, что при поздних тестах, прирост перформанса был минимален). И так поверхность наша, если смотреть на неё “через” треугольники, должна выглядеть вот так, по сравнению с началом:
image image
Значит теперь нам надо её сгенерировать исходя из списка линий, который нам будут передаваться. Создадим объект для этого:

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);

Результат:
image

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

Автор: Alanir

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js