Tutorial Terrain2: Unterschied zwischen den Versionen
Flo (Diskussion | Beiträge) K (→Dateien) |
Flo (Diskussion | Beiträge) K (→Dateien) |
||
Zeile 410: | Zeile 410: | ||
== Dateien == | == Dateien == | ||
* {{DateiLink|file=tut_terrain2_src_vcl|text=Beispiel-Quelltext}} | * {{DateiLink|file=tut_terrain2_src_vcl|text=Beispiel-Quelltext}} | ||
− | * {{DateiLink|file= | + | * {{DateiLink|file=tut_terrain2_exe|text=Beispiel-Programm}} |
Version vom 7. Mai 2009, 13:03 Uhr
Inhaltsverzeichnis
Heightmap Texturen
Vorwort
Hi,
dies ist ein weiteres Tutorial zu Heightmaps. Ich verrate euch heute, wie man Heightmaps ein paar interessante Texturen verpasst. Der erste Teil besteht darin, eine Procedural Texture zu erstellen, die eine Kombination aus der Heightmap und einer Wahl von Bodentexturen ist, z.B. werden die höchsten Punkte der Landschaft mit Schnee bedeckt sein, niedrigere mit Stein, Gras und zum Schluss Wasser.
Der zweite Teil besteht darin, eine Lightmap zu Rendern. Sie enthält die Lichtintensitat der einfallenden Sonne auf der Karte. Damit ihr wisst, wofür die ganze Müh erst einmal ein kleiner Teaser:
Der Trick der Procedural Textures
Wollen wir uns zuerst den Procedural Textures widmen. Die Idee:
Wir haben eine Heightmap. Wir wollen über diese eine Textur ziehen. Je nach Hohe soll eine andere Grundtextur gewählt werden, z.B. Hohenstufe 0 Unterwassertextur. Stufe 60 Sand, Stufe 100 Gras, 170 Stein und 255 Schnee. Daneben wollen wir erreichen, dass die Texturen ineinander übergehe, indem wir immer genau zwischen zwei Stufen die Farbwerte linear interpolieren. Die Grundtexturen werden gekachelt, damit sie nicht so hoch aufgelöst werden müssen, wie die Heightmap selbst.
Ich habe folgende Texturen verwandt:
{{Hinweis|Wenn ihr selbst Texturen erstellen wollt, zeichnet oder fotografiert wild drauf los, und lasst einen Kachel Filter in einem Bildbearbeitungsprogramm drüberlaufen. Von Hand Kacheln erzeugen geht natürlich auch, bedarf aber deutlich mehr Aufwandt.
Bevor wir unsere Textur rendern können, müssen wir uns eine Umgebung schaffen. Wir brauchen eine geladene Heightmap und die Grundtexturen:
type
TProcTexStage = record
Image : TImage;
ScrollBox : TScrollBox;
Stage : Byte;
StageLabel : TLabel;
end;
...
HeightMap : TBitMap;
ProceduralTex : TBitMap;
ProcTexs : Array of TProcTexStage;
...
Der Typ TProcTexStage beinhaltet je eine Grundtextur, geladen in der Image Komponente. Die jeweilige Höhenstufe ist in Stage abgelegt. Die restlichen Objekte sind nur für die GUI gedacht und haben keine weitere Bedeutung für den Algorithmus selbst.
Die Variable ProcTexs enhalt mehrere TProcTexStages. Sie sind bereits nach Stufen sortiert, beginnend mit der kleinsten, wobei gilt: Die niedrigste Stufe ist immer 0, die Hochste immer 255 (wird spater wichtig). Die Mindestzahl der Elemente ist demnach 2. Mehr ist kein Problem, unterschiedliche TProcTexStage sollten aber niemals die selbe Stufe haben.
Sobal diese Annahmen erfüllt sind, können wir beginnen, jedem Pixel der ProceduralTex einen neuen Farbwert zuzuweisen. Das Grundgerüst ist wie folgt:
...
var
X,Y, HeightMapHeight : LongInt;
...
begin
//Texturen sind bereits sortiert...
if Length(ProcTexs) < 2 then
Exit;
//Größe anpassen
ProceduralTex.Width := HeightMap.Width;
ProceduralTex.Height := HeightMap.Height;
for X := 0 to HeightMap.Width - 1 do
for Y := 0 to HeightMap.Height - 1 do
begin
HeightMapHeight :=
Trunc((HeightMap.Canvas.Pixels[X,Y]/clWhite)*255);
//Farbwert zuweisen
...
end
end;
Da wir die Stufen nicht auf bestimmte Werte festgelegt haben (z.B. alle 64 Hohenwerte eine Stufe) müssen wir zunächst das Intervall finden, in welchem die aktuelle Stufe liegt. Die jeweils niedrigere Textur wird in einer Variable Lower abgelegt, die höhere in Upper. Daneben benötigen wir noch einen Satz weiterer Variablen:
var
...
I : Integer;
Upper, Lower : LongInt;
LevelDifference : LongInt; //Levelunterschied zw. Upper u. Lower
LowerDifference : LongInt; //Abstand HeightmapHeight - Lower
LowerRGB, UpperRGB : TRGB; //RGB werte von Upper Lower
Prozent : Single; //Prozent der eingesetzten Farbwerte
FinalRGB : TRGB; //End RGB Wert
...
HeightMapHeight := Trunc(
(HeightMap.Canvas.Pixels[X,Y]/clWhite)*255);
Upper := 0;
Lower := 0;
//Intervall suchen, das für diese Höhenstufe interessant ist
// Schonmal was von binärer Suche gehört?
for I := 1 to Length(ProcTexs)-1 do
if HeightMapHeight <= ProcTexs[I].Stage then
begin
Upper := I;
Lower := I -1;
Break
end;
Assert(Upper <> 0);
LevelDifference := ProcTexs[Upper].Stage - ProcTexs[Lower].Stage;
LowerDifference := HeightMapHeight - ProcTexs[Lower].Stage;
Assert(LevelDifference <> 0);
Prozent := LowerDifference / LevelDifference;
...
Der verwendete Typ TRGB ist ein Array[0..2] of Byte. Wenn ein TColor Wert als Ausgangsmaterial dient, wird dieser mit in ein TRGB umgewandelt:
procedure MakeRGB(Color : TColor; var RGB : TRGB);
begin
Move(Color, RGB[0], 3)
end;
Damit haben wir's fast geschafft. Das einzige was noch fehlt ist die lineare Interpolation zwischen den Farbwerten der Upper und Lower Textur. Dies ist aber an sich nicht weiter schwierig. Man denke an den Mathematikunterricht zurück und erinnert sich an folgende gleichung: y= m * x + t, wobei m die Steigung und t der y-Achsenabschnitt ist:
...
Prozent := LowerDifference / LevelDifference;
MakeRGB(ProcTexs[Lower].Image.Canvas.
Pixels[X mod ProcTexs[Lower].Image.Width,
Y mod ProcTexs[Lower].Image.Height],LowerRGB);
MakeRGB(ProcTexs[Upper].Image.Canvas.
Pixels[X mod ProcTexs[Upper].Image.Width,
Y mod ProcTexs[Upper].Image.Height],UpperRGB);
for I := 0 to 2 do
FinalRGB[I] := Trunc(
(UpperRGB[I] - LowerRGB[I])*Prozent +
LowerRGB[I]);
// y = ( m )* x + t
ProceduralTex.Canvas.Pixels[X,Y] := RGB(FinalRGB[0],
FinalRGB[1],
FinalRGB[2])
Zum Einsatz kam folgende Heightmap:
Jede andere funktioniert natürlich auch, wie z.B. die, aus meinem letzten Tutorial. Der Algorithmus liefert zusammen mit den Oben gezeigten Grundtexturen dieses Bild:
Das Spiel von Licht und Schatten
Wir könnten damit jetzt unsere Welt überziehen. Was dann aber noch fehlt ist Licht und Schatten. Wir wollen Ambient Light und Diffuse Light zum Einsatz kommen lassen. Die Typische Formel zum Berechnen ist:
Intensity=Ambient + Diffuse * (L • N)
L ist ein Vektor zur Lichtquelle, N die Flachennormale. Ambient und Diffuse die Stärken der jeweiligen Lichttypen. Wir wollen abermals alle Punkte der Heightmap durchgehen und unser Ergebnis in LightMaptex ablegen. Wenn ein Punkt nicht im Schatten eines anderen liegt, wird sein Farbwert mithilfe obiger Formel berechnet, ansonsten bekommt der Punkt den Farbwert des Ambient Light zugewiesen.
Die Normal Map
Bevor wir loslegen, legen wir eine Normal Map an, deren Inhalt alle Normalen der Heightmap ist. Diese Komprimieren wir gleich auf Bytes, damit sie nicht Unmengen an Platz beansprucht. Leider kann man die Normalen nur schlecht für einzelne Punkte direkt berechnen, nehmen also Flächen zur Hilfe. Damit die Sache rund wird, betrachten wir aber nicht nur eine Fläche, sondern gleich alle anliegenden und berechnen daraus einen Normalendurchschnitt.
...
type
TNormalMap = Array of Array[0..2] of ShortInt;
...
procedure CreateNormalMap(HM : TBitMap; HeightScale : Single;
var NormalMap : TNormalMap);
begin
...
end;
...
var
NormalMap : TNormalMap;
HeightMapScale : Single;
NormalMap : TNormalMap;
begin
Sun[0] := Cos(LightRotation.Position*Pi/180);
Sun[1] := (LightHeight.Max - LightHeight.Position)/100;
Sun[2] := Sin(LightRotation.Position*Pi/180);
HeightMapScale := HeightScaleEdit.Value;
LightMapTex.Width := HeightMap.Width;
LightMapTex.Height := HeightMap.Height;
CreateNormalMap(HeightMap, HeightMapScale, NormalMap);
CreateLightMap(HeightMap, HeightMapScale, NormalMap, Sun, LightMapTex,
AmbientLight.Position / 100, DiffuseLight.Position/100,
RaytracingCheckBox.Checked)
end;
Dies stellt erst einmal ein Grundgerust dar. Statt einer einzelnen Beschreibung der einzelnen UI Komponenten folgendes Bild:
Nun also zur Funktion CreateNormalMap. Wie gesagt berechnen wir den Durchschnitt der Anliegenden Normalen. Folgendes Schaubild soll die verwendeten Flächen verdeutlichen, wobei v0 das derzeit betrachtete Vertex ist, alle anderen vxs anliegende:
v8 v7•--•--•v1 |8 /|1 /| | / | / | |/ 7|/ 2| v6•-v0•--•v2 |6 /|3 /| | / | / | |/ 5|/ 4| v5•--•--•v3 v4
Mit dem Vektor Kreuzprodukt werden die Normalen für die 8 Flachen berechnet, zusammengezahlt und normiert:
procedure CreateNormalMap(HM : TBitMap; HeightScale : Single;
var NormalMap : TNormalMap);
const
VertexPoss : Array[0..8] of Array[0..1] of ShortInt =
((0,0),(1,-1),(1,0),(1,1),(0,1),(-1,1),(-1,0),(-1,-1),(0,-1));
Faces : Array[0..7] of Array[0..2] of Byte =
((0,1,8),(0,2,1),(0,4,2),(4,3,2),(0,5,4),(0,6,5),(0,8,6),(8,7,6));
var
X,Z : LongInt;
I : LongInt;
Vertieces : Array[0..8] of TVertex;
Normals : Array[0..7] of TVertex;
begin
SetLength(NormalMap, HM.Width*HM.Height);
FillChar(Vertieces[0,0], SizeOf(Vertieces), 0);
for Z := 0 to HM.Height - 1 do
for X := 0 to HM.Width - 1 do
begin
//Vertieces holen
for I := 0 to 8 do
begin
Vertieces[I][0] := X + VertexPoss[I][0]*VertexPosMultiply;
Vertieces[I][2] := Z + VertexPoss[I][1]*VertexPosMultiply;
Vertieces[I][1] := HM.Canvas.Pixels[X + VertexPoss[I][0], Z +
VertexPoss[I][1]]
/clWhite*255*HeightScale
end;
//Normalen holen
for I := 0 to 7 do
begin
Normals[I] := VectorCrossProduct(
VectorNormalize(VectorSubtract(Vertieces[Faces[I][0]],
Vertieces[Faces[I][1]])),
VectorNormalize(VectorSubtract(Vertieces[Faces[I][1]],
Vertieces[Faces[I][2]])));
Normals[I] := VectorNormalize(Normals[I])
end;
//Den "Durchschnittsvektor"...
for I := 1 to 7 do
Normals[0] := VectorAdd(Normals[0], Normals[I]);
Normals[0] := VectorScale(VectorNormalize(Normals[0]),127.0);
NormalMap[Z*(HM.Height) + X][0] := Trunc(Normals[0][0]);
NormalMap[Z*(HM.Height) + X][1] := Trunc(Normals[0][1]);
NormalMap[Z*(HM.Height) + X][2] := Trunc(Normals[0][2])
end
end;
Es werde Licht
Jetzt sind wir soweit. Wir konnen jetzt mit Hilfe unserer Formel fur jeden Punkt die Beleuchtung berechnen. Wie immer: Alle Punkte durchgehen, berechnen:
procedure CreateLightMap(HM : TBitMap; HeightMapScale : Single;
NormalMap : TNormalMap; Sun : TVertex; LightMap : TBitMap;
LightAmbient, LightDiffuse : Single; Raytracing : Boolean);
type
TRay = record
Origin, Direction : TVertex
end;
var
HeightMapData : Array of Array of byte;
procedure PrecacheHeightmap;
var
X,Z : LongInt;
begin
//Läd die Heightmap in ein Delphi Array
SetLength(HeightMapData, HM.Width);
for X := 0 to Length(HeightMapData) -1 do
SetLength(HeightMapData[X], HM.Height);
for Z := 0 to HM.Height - 1 do
for X := 0 to HM.Width - 1 do
HeightMapData[X,Z] := Trunc(HM.Canvas.Pixels[X,Z]/clWhite * 255)
end;
function RayIntersected(Ray : TRay; HM : TBitMap;
HeightMapScale : Single):Boolean;
begin
Result := False;
end;
var
X,Z : LongInt;
Ray : TRay;
n : TVertex;
f : Single;
begin
if Raytracing then PrecacheHeightmap;
//Wir haben paraleles Licht, also bleibt der Richtungsvektor des
//Rays immer gleich
Ray.Direction := VectorNormalize(Sun);
for Z := 0 to HM.Height - 1 do
for X := 0 to HM.Width - 1 do
begin
Ray.Origin[0] := X;
Ray.Origin[1] := HM.Canvas.Pixels[X,Z]/clWhite*255*HeightMapScale;
Ray.Origin[2] := Z;
if RayTracing and RayIntersected(Ray, HM, HeightMapScale) then
f := LightAmbient
else begin
//Komprimierte Normale zurückverwandeln
n[0] := NormalMap[Z*(HM.Height) + X][0];
n[1] := NormalMap[Z*(HM.Height) + X][1];
n[2] := NormalMap[Z*(HM.Height) + X][2];
f := LightAmbient+LightDiffuse*(VectorDotProduct(
VectorNormalize(n),VectorNormalize(Ray.Direction)));
//Zuviel licht?
if f > 1.0 then f := 1.0;
if f < 0.0 then f := 0.0
end;
LightMap.Canvas.Pixels[X,Z] := RGB(Trunc(f*255),
Trunc(f*255),
Trunc(f*255))
end
end;
Argh.... Wieder so ein Monster an Code! Naja, kämpfen wir uns der Reihe mach durch... In HeightMapData wird die Heightmap geladen, um schnelleren Zugriff auf sie zu haben (zur Erinnerung an das 1. Tutorial: GDI Befehle sind arsch langsam). Die entstandenen Daten werden von RayIntersected verwendet um schneller ausführbar zu sein (glaubt mir, hier ist das wirklich nötig!). RayIntersected selbst gibt bisher immer False züruck (wir werden das spater noch ändern), was bedeutet, auf den derzeit betrachteten Punkt wird kein Schatten geworfen.
Bevor ich weitermache erläutere ich kurz das Prinzip von Ray Tracern (z.B.: POV-Ray). Ein Ray ist ein Strahl, der von einem bestimmten Punkt (Origin) losgeschickt, in eine bestimmte Richtung (Direction). Das Tracing, also das abgehen des Rays wird die Funktion RayIntersected für uns übernehmen.
Die Richtung des Rays ist fur alle Punkte gleich, da die Sonne zur Erde einen so großen Abstand hat, dass Licht parallel einfällt. Wird ein Ray geschnitten, wird dem Punkt Origin der Wert des Ambient Light zugewiesen, ansonsten wird mit der vorhin vorgestellten Formel die stärke des einfallenden Lichtes berechnet.
Dies ist noch nichts besonderes, denn OpenGL berechnet das Licht auf ähnliche Weise. Was noch fehlt sind die Schatten. Wollen wir also ein wenig über RayIntersected nachdenken...
Schattenspiele
Wir haben also den Ray. Der Einfachheit halber, gehen wir diesen immer ein Stücken weiter, schauen ob die Höhe der Heightmap gröser ist als der Ray oder nicht, solange bis wir am Ende der Karte angelangt sind. Klingt simpel? Ist es.
function RayIntersected(Ray : TRay; HM : TBitMap;
HeightMapScale : Single):Boolean;
var
Position : TVertex;
Vor : TVertex;
begin
Result := False;
Position := Ray.Origin;
Vor := VectorNormalize(Ray.Direction);
Position := VectorAdd(Position, Vor);
while (Position[0] > 0) and (Position[0] < HM.Width - 1) and
(Position[2] > 0) and (Position[2] < HM.Height - 1) do
begin
if HeightMapData[Trunc(Position[0]), Trunc(Position[2])]*
HeightMapScale >= Position[1] then
begin
Result := True;
Break
end;
Position := VectorAdd(Position, Vor)
end
end;
(*Achtung: In diesem Codestück ist eine kleine Performancebremse eingebaut.
Wer sie findet kommt deutlich schneller an die ersehnte Textur.
Es handelt sich übrigens um eine fehlende Bedingung im while. *)
Das entlanglaufen des Rays ist nich unbedingt Ideal gelöst, aber es erfüllt seinen Zweck. Starten wir einmal unseren nun fertigen Lightmap Generator und betrachten das Ergebnis:
Diese kann man dann mit der Procedural Texture verknupfen... Je Heller ein Punkt in der Lightmap, desto mehr wird von den Farben der Procedural Texture genommen:
Nachwort
Aha, du hast also durchgehalten. Ich hoffe jetzt bald viele schone Landschaften mit Schatten zu sehen. Bin jetzt etwas geschafft vom vielen Tippen.
...have a lot of fun!
Nico Michaelis aka DelphiC
Dateien
|
||
Vorhergehendes Tutorial: Tutorial_Terrain1 |
Nächstes Tutorial: Tutorial_Terrain3 |
|
Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com. Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen. |