Zuerst ein paar Begriffe:
Tile = Ein tile ist ein viereck mit einer Textur-ID, welche angibt mit welcher Textur das Viereck gezeichnet werden soll, sowie vier Eckknoten (=Nodes).
Node = Ein Node ist ein Punkt mit X- und Y-Koordinate und einem Höhenwert Z.
Wichtige Nodes = Die wichtigen Nodes eines Tiles, also diejenigen, welche das Tile benötigt um gezeichnet werden zu können, sind die vier Eckknoten des Tiles sowie zwei zusätzliche Knoten weiter im Süden. (Dazu später mehr)
Als Beispiel nehmen wir eine 3x3 Karte:
Wir haben 9 Tiles (=3x3) und 20 Nodes (4*5)
Die zusätzliche Reihe von Nodes kommt dadurch zustande, dass ein Tile um gezeichnet zu werden noch zwei weitere Nodes weiter im Süden benötigt.
Die Zeichenroutine geht nun wie folgt vor:
- Wir zeichnen alle Tiles von Links nach Rechts, von Oben nach Unten. Diese Reihenfolge ist nötig.
- Um ein Tile zu zeichnen nehmen wir uns die wichtigen Nodes eines Tiles. Das sind die vier Eckknoten des Tiles plus zwei weitere im Süden des Tiles.
- Die X-Position zum Zeichnen eines Knotens ist einfach die X-Position des Knotens selbst. Sie wird nicht verändert.
- Die Y-Position zum Zeichnen eines Knotens wird zusätzlich durch den Z-Wert des Tiles beeinflusst. Mit einem fixen Faktor wird der Z-Wert des Tiles multipliziert und das Ergebnis dann von der Y-Position des Tiles abgezogen. (Falls die linke obere Ecke die Koordinate (0,0) hätte, wäre die untere Ecke (0,0) dann müsste man den Wert addieren anstatt zu subtrahieren.)
- Wir zeichnen nur die vier Eckknoten des Tiles, die zwei zusätzlichen werden zum Zeichnen nicht benötigt.
- Die Farbe mit der ein Eckknoten gezeichnet wird ermittelt sich anhand des Knotens "unter diesem", also der nächste Knoten weiter südlich.
- Hat der südliche Knoten B eines zu zeichnenden Knotens A einen höheren Z-Wert als A wird der Knoten A dunkler gezeichnet anhand der Differenz ihrer Z-Werte.
- Hat der südliche Knoten B eines zu zeichnenden Knotens A einen niedrigeren Z-Wert als A wird der Knoten A heller gezeichnet anhand der Differenz ihrer Z-Werte.
Konklusion:
- Jedes Tile wird als Trapez gezeichnet. Die Linke und rechte Kante eines Tiles sind immer parallel zu einander und orthogonal zur X-Achse der Zeichenfläche.
- Die Y-Koordinate jedes Eckpunktes eines Tiles wird durch den Z-Wert des Eckpunktes weiter nach "oben" oder "unten" verschoben.
- Da sich zwei benachbarte Tiles zwei Eckknoten teilen, können diese sich daher niemals überschneiden.
- Wir müssen von oben nach unten zeichnen (Mit einer Tiefenbuffer einstellung von LEqual, GEqual oder Equal) damit die perspektive erhaltenbleibt und ein einzelnes Tile welches "zu tief" ist nicht weiter südlich liegendere Tiles "überdecken" kann.
- Damit die Lichtberechnung der südlichsten Tiles möglich ist brauchen wir eine zusätzliche Reihe von Knotenpunkten ohne zugehörige Tiles.
- Einzelne Knotenpunkte werden entweder aufgehellt oder verdunkelt in abhängigkeit zu dem Tile welches direkt südlich von diesem liegt.
Hier nocheinmal ein etwas abgewandelter Source-Code mit Kommentaren um das ganze hoffentlich ein wenig verständlicher zu machen:
[ ! Diese zeichenmethode weicht ein wenig von dem oben beschriebenen Konzept ab ! Es wird empfohlen einen Algorithmus zu verwenden welcher das Konzept oben genauer befolgt.
Außerdem verwende ich in diesem kurzen Beispiel noch keinen selbst geschriebenen Shader sondern den standard Shader. Die Hilfsmethode make_lighting(...) wäre mit einem eigenen Shader sehr viel einfacher und effektiver zu lösen.
Der gesamte Code sollte generell nur als ein schneller, nicht perfekter Test angesehen werden. Man kann den Code definitiv an mehreren Stellen verbessern. ]
Code:
public static final void draw(){
if (viewport.is_visible()){
// Ein Viewport ist eine rechteckige Fläche, welche benutzt wird um die Zeichenfläche zu begrenzen und unnötiges zeichnen zu verhindern.
// Ein Viewport übernimmt zudem "Scrollen" von zu zeichnenden Objekten anhand eines x- und y-Scrollwertes.
int viewport_x = viewport.get_x();
int viewport_y = viewport.get_y();
int viewport_scroll_x = (int) viewport.get_scroll_x();
int viewport_scroll_y = (int) viewport.get_scroll_y();
viewport.apply(); // Wrapper um GL-Scissortest
// Übernimmt das skalieren der Texturkoordinaten um exakte Werte angeben zu können. Bindet auch die Textur an den Zeichenkontext.
tex.bind();
GL11.glMatrixMode(GL11.GL_TEXTURE);
GL11.glLoadIdentity();
GL11.glScalef(scale_width, scale_height, 1);
// Initialisiert verschiedene Variablen welche im Zeichenkontext benötigt werden.
Tile tile;
Node[] nodes;
int tile_id;
// Die Loop-Variablen werden benötigt um nur exakt diejenigen Tiles zu zeichnen welche auch mit dem derzeitigen Scroll des Viewports und der Größe der Bildfläche gesehen werden können.
int loop_start_x = viewport_scroll_x / TILE_SIZE_DRAW;
int loop_start_y = viewport_scroll_y / TILE_SIZE_DRAW;
int loop_end_x = loop_start_x + DISPLAY_WIDTH + 1;
int loop_end_y = loop_start_y + DISPLAY_HEIGHT + 1;
if (loop_start_x % TILE_SIZE_DRAW != 0){
loop_start_x -= 1;
}
if (loop_start_y % TILE_SIZE_DRAW != 0){
loop_start_y -= 1;
}
int loop_x;
int loop_y;
// Wird für das Scrollen benötigt.
int draw_x_bonus = viewport_x + loop_start_x * TILE_SIZE_DRAW - viewport_scroll_x;
int draw_y_bonus = viewport_y + loop_start_y * TILE_SIZE_DRAW - viewport_scroll_y;
// Koordinaten zum zeichnen der Eckknoten.
int[] draw_x = new int[4];
int[] draw_y = new int[4];
int draw_z = 0;
// Koordinaten von welchen der Texturausschnitt geladen werden soll.
int src_start_x;
int src_start_y;
int src_end_x;
int src_end_y;
GL11.glBegin(GL11.GL_QUADS);
for (loop_x = loop_start_x; loop_x < loop_end_x; loop_x++){
if (loop_x >= map.get_width()){
break; // Falls die Karte zu klein ist.
}
draw_z = 0; // Beginn einer Spalte, der Z-Wert zum zeichnen wird auf 0 gesetzt.
for (loop_y = loop_start_y; loop_y < loop_end_y; loop_y++){
if (loop_y >= map.get_height()){
break; // Falls die Karte zu klein ist.
}
tile = map.get_tile(loop_x, loop_y);
nodes = tile.get_nodes();
tile_id = tile.get_tile_id();
for (int i = 0; i < 4; i++){
draw_x[i] = draw_x_bonus + nodes[i].get_x();
draw_y[i] = draw_y_bonus + nodes[i].get_y(); // Die get_y()-Methode berücksichtigt bereits den Z-Wert des Tiles welcher multipliziert mit einem festen Faktor von dem Y-Wert des Tiles abgezogen wird.
}
src_start_x = (tile_id % tiles_per_row) * TILE_SIZE;
src_start_y = (tile_id / tiles_per_row) * TILE_SIZE;
src_end_x = src_start_x + TILE_SIZE;
src_end_y = src_start_y + TILE_SIZE;
// Die Zeichenroutine für einen Quad. Wir definieren die vier Vektoren für die Textur und die vier Vektoren zum Zeichnen.
// Die Hilfsmethode make_lighting(...) übernimmt das manipulieren von der Farbe für jeden Eckknoten.
make_lighting(nodes[0], nodes[2]);
GL11.glTexCoord2f(src_start_x, src_start_y);
GL11.glVertex3f(draw_x[0], draw_y[0], draw_z);
make_lighting(nodes[1], nodes[3]);
GL11.glTexCoord2f(src_end_x , src_start_y);
GL11.glVertex3f(draw_x[1], draw_y[1], draw_z);
make_lighting(nodes[3], nodes[5]);
GL11.glTexCoord2f(src_end_x , src_end_y);
GL11.glVertex3f(draw_x[3], draw_y[3], draw_z);
make_lighting(nodes[2], nodes[4]);
GL11.glTexCoord2f(src_start_x, src_end_y);
GL11.glVertex3f(draw_x[2], draw_y[2], draw_z);
draw_z++; // Jedes weitere Tile in der selben Spalte wird mit einem höheren Z-Wert gezeichnet.
}
}
GL14.glSecondaryColor3f(0f, 0f, 0f);
GL11.glColor4f(1f, 1f, 1f, 1f);
GL11.glEnd();
GL11.glDisable(GL11.GL_SCISSOR_TEST);
}
}
// Diese Hilfsmethode berechnet die Werte für GL.primaryColor und GL.secondaryColor anhand eines Tiles und dem Referenztile weiter im Süden.
// Bei verwendung des standard Shaders ist GL.primaryColor eine multiplikative Farbe welche auf den Farbwert eines Fragments angewandt wird,
// GL.secondaryColor ist eine additive Farbe welche auf den Farbwert addiert wird.
// GL.primaryColor kann das Tile also nur verdunkeln da nur Werte zwischen 0.0f und 1.0f erlaubt ist, GL.secondaryColor kann das Tile nur aufhellen
// mit Werten zwischen 0.0f und 1.0f.
private static final void make_lighting(final Node one, final Node other){
// MITTELWERT + (ONE-Z - OTHER-Z) / MAX-Z
float lighting = 0.0f + (one.get_height() - other.get_height()) / 300f;
if (lighting < 0){
// Die Farbe soll verdunkelt werden.
lighting *= -1f; // Werte dürfen nur zwischen 0.0f und 1.0f liegen. Werte außerhalb dieses Bereiches werden von der Grafikkarte automatisch korrigiert.
GL11.glColor4f(lighting, lighting, lighting, 1f);
GL14.glSecondaryColor3f(0f, 0f, 0f);
}else{
// Die Farbe soll aufgehellt werden.
lighting /= 5; // Maximiert den Wertebereich weiterhin. Die Werte sind Geschmackssache.
if (lighting > 0.5f){
lighting = 0.5f;
}
GL11.glColor4f(1f, 1f, 1f, 1f);
GL14.glSecondaryColor3f(lighting, lighting, lighting);
}
}