Autotiling: Automatische Kachelübergänge

Ich bin buchstäblich nur über einen Artikel aus dem Sandkasten gestolpert, in dem es um das Kacheln von Gittern ging, und habe beschlossen, mein eigenes Analogon zu schreiben.
Meine Conversion-Zuweisungsmethode unterscheidet sich geringfügig von der in diesem Artikel genannten.
Der Anfang dieses Systems liegt im berüchtigten Spiel WarCraft III .

Bild

In diesem Screenshot des WC3-Karteneditors sehen Sie die Nähte, die durch rote Pfeile gekennzeichnet sind. Anscheinend ist die Logik der Kacheln in diesem Spiel etwas anders als in den meisten Spielen. Eine Kachel belegt hier nicht eine ganze Zelle . Es ist, als ob an einem Punkt, um den seine Ecken bereits gezeichnet sind.

Dies ist besonders gut bei eingeschaltetem Gitter.

Bild

Normalerweise wird in dieser Situation vorgeschlagen, die Kachel in 4 kleine zu unterteilen. Aber es gibt eine Sache: Was ist in einem solchen Fall zu tun?

Bild

Wann sind alle 4, die die gleichen Quad-Kacheln umgeben, unterschiedlich? Hier sehen Sie deutlich, dass die unterste Kachel den größten Teil davon belegt.
Nachdem ich die Vor- und Nachteile abgewogen hatte, kam ich zu meinem eigenen, ziemlich spezifischen System. Fügen Sie ein neues Netz, ein Übergangsnetz, hinzu. Darin können wir zum Beispiel den int-Typ speichern. In diesem Fall ist es uns möglich, 16 IDs für jedes Quad von Kacheln zu schreiben, die 4 Kacheln mit 16 Übergangsoptionen umgeben. Das ist mehr als genug. Nein, wenn jemand mehr Ausweise benötigt - bitte lange verwenden. Ich entschied, dass 16 Autotails für den Spielort für mich ausreichen würden, der Rest würde ohne Auto-Übergänge sein.

Als nächstes brauchen wir einen Satz Kacheln. Sie können natürlich eine Maske verwenden, aber mit einem Satz Kacheln, sehen Sie, können Sie mit einer guten Fähigkeit (nicht meiner, nein) ein sehr, sehr gutes Bild erzielen.

Bild

Ich selbst habe gerade einen solchen Testsatz von Kacheln gemacht. Es gibt 12 Übergangsoptionen pro Kachel, Sie können Ihre eigenen 4 hinzufügen. Ich habe auch Plätze für zukünftige Variationen von Kacheln reserviert, wie in WC3, aber dieser Teil ist recht einfach und ich werde ihn hier nicht beschreiben.

Wir gehen zum Programmteil über. Zunächst beschreiben wir die Funktionen, die die gewünschte Bitmaske bestimmen, um den richtigen Texturindex auszuwählen. Ich werde sofort reservieren, ich wähle eher nicht standardmäßige Lösungen. Hier wird Java + LWJGL verwendet.

Diese Funktion erstellt eine Bitmaske für dieses Quad. Bit 1 bedeutet, dass sich in dieser Ecke der Kachel eine Kachel daneben befindet (Sie können also verschiedene Kachelsätze gleicher Höhe kombinieren). Oh ja Größe, ich habe es vergessen. Natürlich müssen wir für jede Fliese ihre Höhe bestimmen, um zu wissen, was oben und was unten zu zeichnen ist. Dies wird einfach durch Hinzufügen einer offensichtlichen Variablen gelöst.

	public int getTransitionCornerFor(World world, int x, int y, int height) {
		int corner = 0;
		if (world.getTile(x-1, y).zOrder == height)
			corner |= 0b0001;
		if (world.getTile(x, y).zOrder == height)
			corner |= 0b0010;
		if (world.getTile(x, y-1).zOrder == height)
			corner |= 0b0100;
		if (world.getTile(x-1, y-1).zOrder == height)
			corner |= 0b1000;
		return corner;
	}


Jedes Bit bedeutet seinen eigenen Winkel. 1 Bit ist die linke obere Ecke, 2 ist die linke untere Ecke, 3 ist die rechte untere Ecke und 4 ist die rechte obere Ecke.

Nun zu der Methode, die gewünschten Texturindizes für Übergänge zu bestimmen. Die Methode erwies sich aufgrund meiner Fähigkeiten als umständlich und hässlich. Obwohl speziell für diesen Artikel, habe ich ihn in verschiedene Methoden unterteilt, um nicht zu viele Einrückungen zu verursachen.

public void updateTransitionMap(World world, int x, int y) {
		int w = 16, h = 16;
		int[] temp = new int[4];    //создаем массив, который будет хранить нам 4 угла с 4 битами под ID и 4 битами под переход (т.е. 32 бита в целом для всего тайла)
		for (int i = 0; i < 4; i++)    //на самом деле мне просто было лень нормально разбираться с побитовыми операциями
			temp[i] = 0; 
		if (tileID > 0) {
			for (int i = 1; i <= tilesNum; i++) {
				int corner = getTransitionCornerFor(world, x, y, i);
				int c = 0;
				if (corner > 0) {
					c = setPointTransition(world, temp, x, y, corner, c, i);  //сначала задаем маску для всех углов
					if (c == 3) 
						c = setCornerTransition(world, temp, x, y, corner, c, i);  //потом, если есть 3 смежных(!) угла, соединяем их в один большой
					if (c == 2) 
						c = setEdgeTransition(world, temp, x, y, corner, c, i); //если есть 2 смежных(!) угла, соединяем их в сторону
				}	
			}
		}
	}


Und hier sind die Methoden selbst:

public int setPointTransition(World world, int[] temp, int x, int y, int corner, int c, int i) {
		for (int k = 0; k < 4; k++)
			if ((corner >> k & 1) == 1) {
				int idx = 8+k;
				int storage = 0;
				storage = (idx & 0xF) << 4 | (i & 0xF);
				temp[k] = storage;
				int t = 0;
				for (int l = 0; l < 4; l++) {
					t = (t << 8) | temp[l] & 0xFF;
				}
				world.setTransition(x, y, t);
				c++;
			}
		return c;
	}


Hier ist alles einfach. Wir gehen um jede Ecke und überprüfen das Gebiss. Wenn es eins ist - setze den Index 8 + k , d.h. Winkel (oben habe ich die Anzahl für jede Seite beschrieben (NE, SE, SW, SE)). Als nächstes aktualisieren wir mit einer Krückenmethode durch einen Zyklus unsere Übergangskarte.

Vergessen Sie nicht, die aktualisierte Nummer am Ende anzugeben. Dank Java , das keine einfachen Typen als Referenz ausgibt oder weitergibt.

Methoden zum Verbinden von Punkten mit Ecken und Seiten:
	public int setEdgeTransition(World world, int[] temp, int x, int y, int corner, int c, int i) {
		for (int offset = 0; offset < 4; offset++) {
			boolean isSide = true;
			for (int k = 0; k < 2; k++) { //количество точек у стороны
				if ((corner >> ((k + offset) % 4) & 1) != 1)
					isSide = false;
				else if (k == 1 && isSide)  {
					int idx = (offset+1)%4;
					int storage = 0;
					storage = (idx & 0xF) << 4 | (i & 0xF);
					temp[offset] = storage;
					int t = 0;
					for (int l = 0; l < 4; l++) {
						t = (t << 8) | temp[l] & 0xFF;
					}
					world.setTransition(x, y, t);
				}
			}
		}
		return c;
	}
	public int setCornerTransition(World world, int[] temp, int x, int y, int corner, int c, int i) {
		for (int offset = 0; offset < 4; offset++) {
			boolean isCorner = true;
			for (int k = 0; k < 3; k++) { //количество точек у угла
				if ((corner >> ((k + offset) % 4) & 1) != 1)
					isCorner = false;
				else if (k == 2 && isCorner)  {
					int idx = 4+offset;
					int storage = 0;
					storage = (idx & 0xF) << 4 | (i & 0xF);
					temp[offset] = storage;
					int t = 0;
					for (int l = 0; l < 4; l++) {
						t = (t << 8) | temp[l] & 0xFF;
					}
					world.setTransition(x, y, t);
				}
			}
		}
		return c;
	}


Dies ist genau das gleiche Prinzip. Der einzige Unterschied ist die Startnummer des Texturindex, so dass wir den gewünschten Zyklus nehmen können, der den Versatz festlegt, dh von welchem ​​Punkt aus der Winkel beginnt. Überprüft den benachbarten Winkel (oder die Seite) gegen den Uhrzeigersinn, beginnend an diesem Punkt. Wenn mindestens ein Punkt kein benachbartes Plättchen ist, brechen wir ab, es wird weder der Winkel noch die Seite erhalten.
Das ist alles, wir haben eine Übergangskarte erstellt! Es gibt 5 Bits pro Kachel. Eine zum Speichern einer Kachel (256 mögliche Variationen) und ein Bit für jede Ecke zum Speichern von Metadaten.

Es bleibt nur, um dieses Geschäft zu machen. Ich werde die alte veraltete Methode im Sofortmodus betrachten (ich habe vor, zu VBO zu wechseln, jetzt muss ich ein wenig über die Struktur und das dynamische Update von VBO wissen und nur den sichtbaren Teil rendern).

Nun, es gibt nichts Kompliziertes:

public void renderTile(World world, int x, int y) {
		int w = 16, h = 16;
		int s = 0;
		if (tileID > 0) {
			for (int i = 0; i < 4; i++) {
				int t = world.getTransition(x, y);
				int src = ((t >> (3-i)*8) & 0xFF);
				int idx = src >> 4 & 0xF;
				int id = src & 0xF;
				int u = (idx%8)*16, v = 16 + 16*(idx/8) + (id-1)*48,
				u1 = u + w, v1 = v + h;
				if (id != 0) {
					GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, u, v, u1, v1); //не обращайте внимания на хардкод, всё равно будет переписан под VBO 
				}
			}
		}
	}


Was machen wir hier Ja, wir gehen alle 8 Bits durch und erhalten die ersten 4 und letzten 4 für ID und Übergang. Als nächstes übergeben wir die OpenGL-Parameter, es verteilt bereits das Rendering.

Ergebnis:

Bild
(Ja, die in Swing integrierte LWJGL-Zeichenfläche).

Anscheinend haben wir etwas vergessen? Zeichne ein ganzes Stück Fliese, wenn 4 umgebende Punkte in der Höhe ähnlich sind!

public void renderTile(World world, int x, int y) {
	int w = 16, h = 16;
	int s = 0;
	if (tileID > 0) {
		int c = 0;
		for (int i = 0; i < 4; i++) {
			int t = world.getTransition(x, y);
			int src = ((t >> (3-i)*8) & 0xFF);
			int idx = src >> 4 & 0xF;
			int id = src & 0xF;
			int u = (idx%8)*16, v = 16 + 16*(idx/8) + (id-1)*48,
			u1 = u + w, v1 = v + h;
			if (id != 0) {
				if (id == tileID)
					c++;
				GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, u, v, u1, v1);
			}
		}
		if (c == 4) {
			GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, 0, 48*(tileID-1), 16, (tileID-1)*48+16);
		}
	}
}


Bild

Fehlt etwas? Richtig, wir müssen entscheiden, wie die untere Kachel gezeichnet werden soll. Um ehrlich zu sein, habe ich es fast durch Zufall geschafft, aber es ist dieser Moment, der noch verbessert werden muss. Dies kann zwar als verschraubte Krücke angesehen werden, hat aber keinen Einfluss auf das Ergebnis.

Lassen Sie uns unsere Methode ein wenig ändern:

	public void renderTile(World world, int x, int y) {
		int w = 16, h = 16;
		int s = 0;
		if (tileID > 0) {
			for (int i = 1; i <= tilesNum; i++) {
				int corner = getTransitionCornerFor(world, x, y, i);
				int c = 0;
				if (corner > 0) {
					for (int k = 0; k < 4; k++)
						if ((corner >> k & 1) == 1) {
							c++;
						}
				}
				boolean flag = false;
				int fill = getFillCornerFor(world, x, y, i);
				if (fill > 0)
					for (int k = 0; k < 4; k++)
						if ((fill >> k & 1) == 1) {
							c++;
							if (k == 4 && c == 4)
								flag = true;
						}
				if (c == 4) {
					GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, 0, 48*(i-1), 16, (i-1)*48+16);
					if (flag)
						break;
				}
			}
			for (int i = 0; i < 4; i++) {
				int t = world.getTransition(x, y);
				int src = ((t >> (3-i)*8) & 0xFF);
				int idx = src >> 4 & 0xF;
				int id = src & 0xF;
				int u = (idx%8)*16, v = 16 + 16*(idx/8) + (id-1)*48,
				u1 = u + w, v1 = v + h;
				if (id != 0) {
					GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, u, v, u1, v1);
				}
			}
		}
	}


Eine andere Methode wurde hinzugefügt. Es ist fast äquivalent zu einer Methode, die Bits benachbarter Kacheln schreibt. Da ist er:

	public int getFillCornerFor(World world, int x, int y, int height) {
		int corner = 0;
		if (world.getTile(x-1, y).zOrder > height)
			corner |= 0b0001;
		if (world.getTile(x, y).zOrder > height)
			corner |= 0b0010;
		if (world.getTile(x, y-1).zOrder > height)
			corner |= 0b0100;
		if (world.getTile(x-1, y-1).zOrder > height)
			corner |= 0b1000;
		return corner;
	}


Es werden alle Kacheln im Distrikt bestimmt, deren Höhe größer ist als die Höhe der übertragenen Kachel.

Das heißt Wir iterieren über alle Kacheln für eine bestimmte Zelle (natürlich sollten nur Autotiles sortiert werden) und sehen, wie viele Kacheln über dieser liegen. Vergessen Sie nicht, dass wir vorher die Anzahl der von diesem Plättchen abgedeckten Punkte zählen. Wenn die Anzahl der Punkte einer bestimmten Kachel + die Summe der Punkte anderer Kacheln, die eine bestimmte Kachel überlappen, == 4, dann zeichnen wir ein vollständiges Quad mit dieser Textur und brechen die Schleife. Das sind Krücken.

Das Ergebnis ist hervorragend:

Bild

Vielleicht ist das alles.

PS Warum ist diese Methode besser als das? Nun, WC3 zeigt deutlich, dass Sie mit einem solchen System eine Landschaft von unvorstellbarer Schönheit erreichen können. Persönlich scheint es mir flexibler zu sein, was jedoch einige Schwierigkeiten bei der Umsetzung mit sich bringt. Und ja, es bedarf noch einiger Verfeinerungen, wie ich oben sagte.

Jetzt auch beliebt: