© DrHitch/Shutterstock.com
Erfolgreiche Spieleentwicklung

4 Landschaften und Vegetation rund ums Jahr


In diesem Kapitel stehen bei der Ausformung der Spielewelt Kreativität und Fleißarbeit im Vordergrund. Dabei geht es um nicht weniger als die Erschaffung von Wüsten, Wäldern, Steppen, Wasserflächen und Gebirgslandschaften – und um die Vegetationsdarstellung im Wechsel der Jahreszeiten.

Von der Minecraft-Euphorie beflügelt, gehen wir nunmehr seit drei Kapiteln der Frage nach, mithilfe welcher Techniken sich optisch ansprechende blockbasierte Spielewelten prozedural erzeugen und darstellen lassen. Zugegeben, auf den ersten Blick mag das Thema für den einen oder anderen Leser nicht gerade sonderlich spannend klingen, zumal auch die bislang veröffentlichten Screenshots unserer kleinen Demoanwendung recht unspektakulär wirkten. Nun denn, diese Phase gehört, wie die Abbildungen 3.1 bis 3.7 recht ansehnlich demonstrieren, mittlerweile endgültig der Vergangenheit an. Durch eine von der Jahreszeit beeinflusste Vegetations- und Landschaftsdarstellung, durch unterschiedliche, vom Breitengrad (Äquator: nullter Breitengrad, Süd- bzw. Nordpol: -/+ 90. Breitengrad) abhängige Vegetationsformen und nicht zuletzt dank abwechslungsreicher Landschaftstypen, darunter Wüsten, Wälder, Steppen, Wasserflächen oder Gebirgslandschaften, wirken die Spielewelten inzwischen sehr viel ansprechender und realistischer.

rudolph_opengl_1.jpg

Abbildung 3.1: Blockbasierte Spielewelt (Getreideanbau)

rudolph_opengl_2.jpg

Abbildung 3.2: Blockbasierte Spielewelt (im Sommer)

rudolph_opengl_3.jpg

Abbildung 3.3: Blockbasierte Spielewelt (im Sommer)

rudolph_opengl_4.jpg

Abbildung 3.4: Blockbasierte Spielewelt (im Herbst)

rudolph_opengl_5.jpg

Abbildung 3.5: Blockbasierte Spielewelt (im Herbst)

rudolph_opengl_6.jpg

Abbildung 3.6: Blockbasierte Spielewelt (im Winter)

rudolph_opengl_7.jpg

Abbildung 3.7: Blockbasierte Spielewelt (im Winter)

Wie dem auch sei, der Weg hierher war mitunter ein wenig mühsam und steinig. Anfangs standen Überlegungen zur Speicherverwaltung – Lauflängenkodierung der Spieleweltdaten einerseits, Arrays mit den unkomprimierten Daten der potenziell sichtbaren Blöcke andererseits – im Vordergrund. Im Anschluss daran haben wir uns die Frage gestellt, wie sich die Lichtausbreitung in der Spielewelt CPU-basiert simulieren lässt und wie sich die einzelnen Programmabläufe auf mehrere Worker-Threads – BlockWorld_VisibilityTestThread(), BlockWorld_LightingCalculationsThread() sowie BlockWorld_LocalBlockArrayUpdateThread() – verteilen lassen. Einen großen Stellenwert hat ferner die Behandlung der einzelnen Renderingtechniken eingenommen, die für eine effiziente und ruckelfreie Darstellung unserer blockbasierten Spielewelt praktisch unverzichtbar sind. Hierzu zählt:

  • die Minimierung der Anzahl der erforderlichen Draw Calls mittels Geometry Instancing
  • die Verwendung von Texture-Buffer-Objekten zum Zwischenspeichern der Block-Instanzdaten und Helligkeitswerte
  • der Gebrauch von Texture-Array-Objekten zur Vermeidung von Texturwechseln
  • die Durchführung von Beleuchtungsberechnungen im Verlauf der Post-Processing-Phase (Deferred Rendering)

Im vorangegangenen Teil unserer Kapitelserie haben wir uns unter anderem mit den für die prozedurale Landschaftsgestaltung so wichtigen (zweidimensionalen) Noise-Berechnungen auseinandergesetzt und in diesem Zusammenhang eine Reihe von Funktionen für die Generierung von ganz unterschiedlichen Rauschmustern implementiert:

  • Value Noise ohne Interpolation der Rauschwerte:
inline void ValueNoise2D_NoInterpolation(long* pResult, float &x, float &z,
float &frequency, long &minNoiseValue, long &maxNoiseValueDiff) {...}
  • Value Noise mit linearer Interpolation der Rauschwerte:
inline void ValueNoise2D_LinearInterpolation(long* pResult, float &x, float &z,
float &frequency, long &minNoiseValue, long &maxNoiseValueDiff) {...}
  • Value Noise mit kosinusförmiger Interpolation der Rauschwerte:
inline void ValueNoise2D_CosineInterpolation(long* pResult, float &x, float &z,
float &frequency, long &minNoiseValue, long &maxNoiseValueDiff) {...}
  • Gradientenbasiertes Rauschen (Perlin Noise) mit linearer Interpolation:
inline void GradientNoise2D_LinearInterpolation(long* pResult, float &x,
float &z, float &frequency, long &minNoiseValue,
long &maxNoiseValueDiff) {...}
  • Gradientenbasiertes Rauschen (Perlin Noise) mit kosinusförmiger Interpolation:
inline void GradientNoise2D_CosineInterpolation(long* pResult, float &x,
float &z, float &frequency, long &minNoiseValue,
long &maxNoiseValueDiff) {...}
  • Cell Noise (Worley Noise):
inline void CellNoise2D(long* pResult, float &x, float &z, float &frequency,
long &minNoiseValue, long &maxNoiseValueDiff, long &structure) {...}

Landschaftsgestaltung mithilfe von 2D-Noise-Funktionen

Gegen Ende des letzten Kapitels sind wir bereits darauf zu sprechen gekommen, wie wir die zuvor skizzierten Rauschfunktionen bei der Generierung der Terrain-Höhenwerte einsetzen können. Sämtliche Parameter, die für die Durchführung der in Listing 3.1 skizzierten Berechnungsschritte erforderlich sind, speichern wir neben einer Vielzahl von weiteren Daten in einer so genannten World-Description-Datei in der in Listing 3.2 gezeigten Weise ab.

SumOfHeightValues = 0;

for(i = 0; i < ProceduralTerrainDesc->NumHeightCalculationSteps; i++)
{
if(ProceduralTerrainDesc->HeightNoiseDescList[i] == 0)
{
ValueNoise2D_CosineInterpolation(&HeightValue, ixWorld, izWorld,
ProceduralTerrainDesc->HeightFrequencyList[i],
ProceduralTerrainDesc->MinRelativeHeightList[i],
ProceduralTerrainDesc->MaxHeightDifferenceList[i]);
}
else if(ProceduralTerrainDesc->HeightNoiseDescList[i] == 1)
GradientNoise2D_CosineInterpolation(...);
else
CellNoise2D(...);

HeightValue = min(HeightValue, ProceduralTerrainDesc->MaxRelativeHeightList[i]);

if(ProceduralTerrainDesc->HeightCombinationParameterList[i] == 1)
SumOfHeightValues -= (ProceduralTerrainDesc->HeightWeightFactorList[i]*
HeightValue);
else if(ProceduralTerrainDesc->HeightCombinationParameterList[i] == 2)
SumOfHeightValues = max(SumOfHeightValues,
ProceduralTerrainDesc->HeightWeightFactorList[i]*
HeightValue);
else if(ProceduralTerrainDesc->HeightCombinationParameterList[i] == 3)
SumOfHeightValues = min(SumOfHeightValues,
ProceduralTerrainDesc->HeightWeightFactorList[i]*
HeightValue);
else
SumOfHeightValues += (ProceduralTerrainDesc->HeightWeightFactorList[i]*
HeightValue);
}

SumOfHeightValues += ProceduralTerrainDesc->HeightValueOffset;
SumOfHeightValues *= HeightModificationFactor1;
SumOfHeightValues *= HeightModificationFactor2;
HeightArray[ix+iChunkSize*iz] = SumOfHeightValues;

Listing 3.1: Modellierung der Geländeform (Terrainhöhenwerte)

#Height Value Offset:# 70
#Num Height Calculation Steps:# 1

#Calculation Step 1#
#Value Noise (0), Gradient Noise (1), Cell Noise (2):# 1
#Min relative (to Height Value Offset) Height:# 100
#Max Height Diff:# 70
#Max relative Height:# 200
#Frequency:# 0.04
#Additional Noise Parameter:# 1
#Weight Factor:# 1.0
#Add (0), Subtract(1), Max(2), Min(3):# 0

Listing 3.2

Nach genau dem gleichen Prinzip können wir selbstverständlich auch alle weiteren Charakteristika unserer Spielewelt generieren, so zum Beispiel wie in Listing 3.3 demonstriert, die Ausmaße der einzelnen Laubwaldregionen. Die Anzahl der für die Rauschberechnungen erforderlichen Parameter bleibt gleich, lediglich ihre Bedeutung verändert sich ein wenig (Listing 3.4).

SumOfBroadleafForestAreaValues = 0;

for(i=0; i < ProceduralTerrainDesc->NumBroadleafForestAreaCalculationSteps; i++)
{
if(ProceduralTerrainDesc->BroadleafForestAreaNoiseDescList[i] == 0)
ValueNoise2D_CosineInterpolation(...);
else if(ProceduralTerrainDesc->BroadleafForestAreaNoiseDescList[i] == 1)
GradientNoise2D_CosineInterpolation(...);
else
CellNoise2D(...);

BroadleafForestAreaValue = min(BroadleafForestAreaValue,
ProceduralTerrainDesc->MaxBroadleafForestAreaValueList[i]);

if(ProceduralTerrainDesc->BroadleafForestAreaCombinationParameterList[i] == 1)
SumOfBroadleafForestAreaValues -= (ProceduralTerrainDesc->
BroadleafForestAreaWeightFactorList[i]*BroadleafForestAreaValue);
else if(ProceduralTerrainDesc->
BroadleafForestAreaCombinationParameterList[i] == 2)
SumOfBroadleafForestAreaValues = max(SumOfBroadleafForestAreaValues,
ProceduralTerrainDesc->BroadleafForestAreaWeightFactorList[i]*
BroadleafForestAreaValue);
else if(ProceduralTerrainDesc->
BroadleafForestAreaCombinationParameterList[i] == 3)
SumOfBroadleafForestAreaValues = min(SumOfBroadleafForestAreaValues,
ProceduralTerrainDesc->BroadleafForestAreaWeightFactorList[i]*
BroadleafForestAreaValue);
else
SumOfBroadleafForestAreaValues += (ProceduralTerrainDesc->
BroadleafForestAreaWeightFactorList[i]*BroadleafForestAreaValue);
}

if(SumOfBroadleafForestAreaValues > ProceduralTerrainDesc->
BroadleafForestAreaSizeValue)
BroadleafForestAreaArray[ix+iChunkSize*iz] = 0;
else
BroadleafForestAreaArray[ix+iChunkSize*iz] = 1;

Listing 3.3: Modellierung der Laubwaldregionen

#Noise (Broadleaf Forest Area) Size Value:# 10
#Num Noise (Broadleaf Forest Area) Calculation Steps:# 1

#Calculation Step 1#
#Value Noise (0), Gradient Noise (1), Cell Noise (2):# 1
#Min Noise (Broadleaf Forest Area) Value:# 0
#Max Noise (Broadleaf Forest Area) Diff:# 12
#Max Noise (Broadleaf Forest Area) Value:# 100
#Frequency:# 0.05
#Additional Noise Parameter:# 1
#Weight Factor:# 1.0
#Add (0), Subtract(1), Max(2), Min(3):# 0

Listing 3.4

Sämtliche Parameter, die für die Generierung der in den Abbildungen 3.1 bis 3.7 gezeigten Spielewelt erforderlich sind, finden Sie in der World-Description-Datei ProceduralWorldDesc.txt im Datenordner unserer Demoanwendung. Riskieren wir doch einfach mal einen Blick auf ihren Inhalt.

Im ersten Schritt legen wir zunächst die Klimazone fest, innerhalb der sich unsere Spielewelt befinden soll. Der Wert dieses Parameters wird im weiteren Verlauf der Programmentwicklung erheblichen Einfluss auf die Wetterbedingungen, die Vegetationsdarstellung sowie den Verlauf der Jahreszeiten haben und sich darüber hinaus auch in den prozedural erzeugten Geländeformationen widerspiegeln:

#Climate Zone - Polar (0), Cold Temperate (1),
Warm Temperate (2), Subtropical (3), Tropical (4):# 2

Die scheinbaren Sonnenbahnen am Himmel (Tag-Nacht-Wechsel) berechnen wir in Abhängigkeit vom virtuellen Breitengrad (Äquator: nullter Breitengrad, Süd- bzw. Nordpol: -/+ 90. Breitengrad) unserer Spielewelt unter Berücksichtigung des Datums bzw. der zugehörigen Jahreszeit. Sämtliche hierfür erforderlichen Daten werden in einer separaten Datei gespeichert:

#DayNightCycleDescFile:# ../Data/DayNightCycle_WarmTemperateZone.txt

#NrOfDay:# 122
#Info: NrOfDay: 172: 21. Juni, NrOfDay: 355: 21. Dezember#
#Season (0:Spring Or Summer, 1:Autumn, 2:Winter):# 0

Im Anschluss daran teilen wir die Spielewelt in verschiedene Höhenbereiche auf. Mit den ersten beiden Parametern legen wir zunächst fest, in welcher Tiefe man beim Bergbau spätestens auf Lava trifft und in welcher Höhe sich der Wasserspiegel sämtlicher Flüsse, Seen und Meere befindet:

#BaseLavaLevelBlockHeight:# 10
#SeaLevelBlockHeight:# 63

Die Wolkendarstellung steht momentan noch auf unserer To-do-Liste. Ganz im Stil von Minecraft setzen wir anstelle von einfachen Billboard-Objekten auf dreidimensionale Wolkenformationen, die sich innerhalb des nachfolgend definierten Höhenbereichs befinden werden:

#CloudsLowerBlockHeight:# 128
#CloudsUpperBlockHeight:# 132

Nach der Aufteilung in unterschiedliche Höhenbereiche legen wir fest, wie sich der Untergrund mit zunehmender Tiefe verändern soll. Da wir uns im Rahmen der Programmentwicklung bislang hauptsächlich mit der Modellierung der Landschaftsoberfläche befasst haben, geben wir an dieser Stelle lediglich an, in welcher Tiefe das lockere Erdreich (Erde, Sand oder Lehm) durch kompakte Gesteinsschichten abgelöst wird:

#MinSoftGroundDepth:# 2
#MaxSoftGroundDepth:# 4

Kommen wir nun zu den 2D-Noise-Berechnungen. Im ersten Schritt definieren wir zunächst, wie stark sich die lokalen Schneefall- (sämtliche Blöcke sind ab dieser Höhe mit einer Schneedecke überzogen) und Vegetationsgrenzen (ab dieser Höhe sind keine Gras- oder Steppenblöcke mehr anzutreffen) von Ort zu Ort unterscheiden sollen. Möchte man hingegen, dass die jeweiligen Höhengrenzen in der Spielewelt überall einheitlich sind, muss man für die nachfolgend gezeigten Schwankungsparameter (Varianz) einen Wert von null festlegen:

#SnowLevelBlockHeight:# 80
#SnowLevelBlockHeightVariance:# 1.0
[Noise-Parameter]
#MaxVegetationBlockHeight:# 75
#MaxVegetationBlockHeightVariance:# 0.95
[Noise-Parameter]

Für die Modellierung der Geländeform verwenden wir momentan drei Sätze von Rauschmustern, die wir gemäß Listing 3.1 miteinander verrechnen:

#Height Value Offset:# 70
[Noise-Parameter]
#Height Modification Factor 1 Threshold:# 255
[Noise-Parameter]
#Height Modification Factor 2 Threshold:# 68
[Noise-Parameter]

Im nächsten Schritt geht es an die Ausgestaltung der Ackerflächen. Gras- und Erdb...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang