PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Noises und infinite Levels (Minecraft Style) (Voxel) Umsetzung?



eXi
19.01.2014, 21:31:32
Hey Leute,

ganz am Anfang sei gesagt, bevor jemand umsonst weiter liest:

Dieser Post dient der Diskussion zu dem Thema im Titel.

Ich suche keine konkrete Hilfe, da ich nicht denke, dass jemand von euch sich damit schon beschäftigt hat.

Da ihr aber alle recht wissenshunrig und diskussionsfreudig seid, fände ich es nett, wenn ihr vielleicht eure Ideen, Ratschläge und vielleicht sogar euer Wissen hier teilen könntet.
Gleichermaßen teile ich gerne mein Wissen mit euch, vor allem in Form dieses Threads.

Um das Thema mal konkret zu nennen: Die Erstellung einer Blockwelt, wie in Minecraft, welche zufällig erzeugt wird und endlos ist. (C# coding, Pseudecode)

Wen das also nicht interessiert, der sollte nicht weiterlesen.

Nun aber los.

Ich bin momentan gedanklich ein wenig dabei mir ein Projekt für zwischendurch zu suchen. Einfach um mal nicht Ricochet zu machen und trotzdem etwas zum knobeln zu haben.
Mein Auge fällt da recht schnell auf Voxelspiele. Also um es einfach auszudrücken auf Minecraft.

Die Idee dahinter ist ja das Verfahren Noise (Unruhe) auf die Erstellung der Welt anzuwenden. Also eine Art "zufällige" Höhenkarte, wie man sie aus dem Atlas vielleicht kennt, auf die Höhe der Welt anzuwenden.

Hier mal ein Bild einer solchen Noisemap.


http://29a.ch/sandbox/2010/fire/noise.jpeg


Demnach geht man hin und definiert einen Chunk (ein Teilstück der Map, welches immer wieder geladen bzw gespeichert wird um die endlose Map darzustellen) mit zum Beispiel der Größe 20x20x20 Blöcken (1 Block 1x1x1 Units).
Ohne Noise wäre das einfach "ein" Block 20x20x20 Units. Die Noisemap ändert dann allerdings die Höhen nach Graustufen.
Lässt man nun in einer Renderreichweite, von zB 80 Units, Chunks laden, hätte man schon mal eine große Fläche mit Höhenunterschieden.
Wenn ich das soweit richtig verstanden habe, lässt sich dort ein "Seed" einbauen, welcher bei gleicher Angabe die Welt immer gleich aussehen lässt, da die Verschiebung der Noisemap dann immer gleich ist.

Soweit so gut. Jetzt kommt aber der schwierigere und interessantere Teil.

Gehen wir mal davon aus wir haben 9 Chunks geladen. Also 3x3 Chunks mit jeweils 20x20x20 Blöcken und diese sind mit einer Noisemap erstellt.

Nun fängt es an in meinem Kopf zu qualmen, denn angrenzende Chunks mit gleichen Übergängen zu erstellen, will mir nicht so recht in den Sinn kommen. Außerdem möchte ich auch unterschiedliche Blöcke innerhalb dieser Chunks haben.

Also im Endeffekt, damit man sich das als Normalmensch mal vorstellen kann, wären das hier zB 9 Chunks:

http://marchingcubes.com/minecraft_sauer_blender_256.jpg


Was man relativ schnell sieht ist, dass man nicht direkt einen Block nimmt, sondern seine Faces (bzw die beiden Vertices eines Faces) rendert und zwar nur dann, wenn kein Face an ein anderes grenzt.
Alles was unterhalb der Heightmap liegt, ist auch gar nicht erst vorhanden. Somit ist das, um es genauer auszudrücken, eine Fläche mit Würfelförmigen Erhöhungen (definiert durch die Noisemap). Die Bäume denken wir uns mal weg.

Was auch noch auffällt ist, das es unter der normalen Bodenfläche Höhlen gibt. Nur aus Stein. Wenn ich mich nicht irre, dann ist das exakt die Art wie Minecraft momentan funktioniert und aussieht wenn man nur bestimmte Chunks hat, bzw wenn mal wieder der PC beim Rendern unter Java nicht nach kommt und man durch den Boden gucken kann.

Gräbt man sich nun einen Block tiefer, so werden dort die alten Faces gelöscht und neue gerendert, weil ja keine weiteren mehr angrenzen.

Soweit zum Verständnis, was das Resultat ist. Nun kommt aber mein Eingeschränktes Verständnis von Noises und Chunks ins Spiel. Denn wenn ich nach dem gezeigten Bild gehe, weiß der Chunk wie hoch die einzelnen Schichten sind und ersetzt dort die Textur und vor allem den Block Typ. Ich komme aber nur soweit, das ich alles aus einem Blocktypen erstelle. Also eine grüne Masse.
Desweiteren hätten wir, wenn wir zum Beispiel auf dem Bild unser Auge auf den untersten Chunk werfen, würden wir nun einen weiteren Chunk dort unten links dran packen, bei dem Berg in den man so schön reingucken kann, eine grade abgeschnittene Fläche. Auf dem Bild sind die Übergänge zwischen den Chunks ja schon richtig, also würde das wohl nicht passieren. Das Wasser außer Acht gelassen, da ich dort noch keinen Gedanken dran verschwenden wollte.

Also mein Problem, um den ganzen Text mal auf einen Nenner zu bringen, ist der Übergang zwischen Chunks und das nutzen verschiedener Blocktypen. Auch das Verständnis von Noises müsste erweitert werden.
Wenn ich so eine Noisemap selber machen würde, müsste ich sie ja für jedes Biom (rein theoretisch) unterschiedlich machen. Also flach für Wüste und große Unterschiede für Berge.

Mich würde jetzt, wie ganz oben geschrieben, eure Meinungen, Ideen, Erfahrung und euer Wissen dazu einmal interessieren.

Wie würdet ihr das machen, habt ihr Ideen zu flüssigen Übergängen zwischen den Chunks?
Wie könnte man so einen Chunk nach Höhen überprüfen? (Bei Minecraft ist zum Bespiel bekannt, auf welcher Höhe es Diamanten geben kann)
Wie könnte man andere Blöcke einfügen, welche von der Höhe abhängen und ihr Anzahl und ihr Vorkommen rar machen?

Angemerkt sei hier, das ich natürlich in Unity programmiere. Das ist für euch aber nicht vorausgesetzt. Es reicht Pseudocode, falls ihr Ideen habt. Unity arbeitet mit Vektoren. Demnach sind Punkt der Blöcke und Vertices festgelegt.

Meine Idee zur Lösung ist momentan für jedes größere Gebiet eine große Noisemap zu erstellen und diese Abschnittweise für die Chunks zu nutzen. Am Rand der Noisemap, setzen wir eine fixe "Höhe" welche den Übergang zwischen zwei Biomen darstellt. Allerdings weiß ich nicht, wie das mit dem Seed dann funktionieren soll. Eine Verschiebung der Abschnitte auf der Noisemap würde nicht so viel Unterschied einbringen.
Eine andere Idee wäre, die Chunks überlappen zu lassen von der Noisemap her. Das ist aber nur eine Idee in meinem Kopf und ich weiß gerade nicht wie das Umzusetzen ist.

Naja, falls jemand von euch bis hier unten gelesen hat, würde ich mich über eine Antwort freuen. Vielleicht bleibt der Thread ja auch leer und ich knobel alleine weiter :'(

eMo
19.01.2014, 22:36:23
1.) Du lädst einfach immer 27 (3³) Chunks, so hast du einen fließenden Übergang zwischen den Chunks ;)
2.) Du speicherst die Chunkkoordinate mit oder gibst jedem, mit Textur belegtem, Voxel eine Koordinate mit. Ab einem bestimmten Z-Wert gibt es Diamant. Einfacher wäre wahrscheinlich ein Chunkkoordinatensystem.
3.) Zufallszahlen. Wenn du auf einen neuen Chunk triffst, wird dieser sofort komplett generiert. Wird dabei bei der Koordinate (x,y,z) der Zufallswert von 0,99 überschritten (Zufallszahl zwischen (0,1) (ja, offenes Intervall, obwohl irrelevant, da Eintrittswahrscheinlichkeit in lR sowieso 0)) wird mit einer Wahrscheinlichkeit von 0,5^x, wobei x die Anzahl der erfolgreichen Durchgänge, in einer Koordinate außenrum ein weiterer Block des Blocktyps erstellt.
Allgemein gehst du bei Erstellung des Chunks schichtweise vor, wobei die Wahrscheinlichkeit auf Luft immer weiter abnimmt (natürlich nur am obersten Chunk), unterirdische Chunks mit negativer Z-Koordinate haben dann auch einen Blocktyp Luft mit bestimmter Wahrscheinlichkeit.
Ist dein Chunk fertig, glättest du ihn, sodass eine halbwegs nutzbare Fläche entsteht. Hierbei gehst du natürlich über die Chunkgrenzen hinaus, um auch außerhalb des Chunks zu glätten. Unbenutzte Chunks löscht du aus dem Speicher, speicherst sie aber ab und lädst die im Notfall wieder ;)

Nilo
19.01.2014, 23:16:43
eMo´s Lösungsvorschläge halte ich für sehr vielversprechend!
Als erstes sind mir alte Videos von frühen Minecraft-Versionen in den Sinn gekommen. Da schien es genau die gleichen Probleme zu geben (Seeds, unterschiedliche Blöcke und Übergänge zwischen Chunks)
Es gab früher noch keine Seeds, deutlich mehr abgeschnittene Berge und in einer der ersten Versionen auch noch Tests mit den unterschiedlichen Blöcken.
Hier gibt es ein Video zum Entwicklungsverlauf in dem du das am Anfang ganz gut siehst: http://www.youtube.com/watch?v=W_-vFa-IyB8

eXi
19.01.2014, 23:17:57
Gehen wir mal davon aus ich erstelle die Chunks über 3 for Schleifen von 0 bis 19, also 20 in alle Dimension. Dann müsste ich also die Position im Koordinatensystem überprüfen und ab einer bestimmten Höhe über eine if Abfrage die Zufallsvariable nutzen.

Dennoch weiß ich nicht wie ich Chunks, welche "zufällig" über die Noisemap erstellt werden, fließend übergehen lassen soll. Der neue Chunk müsste ja demnach an der nächsten Stelle der Noisemap weitermachen. Irgendwann komm ich aber doch an den Punkt wo sich etwas wiederholt oder nicht?
Wie gesagt, Noisemaps und ihre Benutzung hab ich noch nicht verstanden, obwohl ich mir schon Tuts reingezogen habe. Vielleicht denke ich auch zu quer.

EDIT: @Nilo: Sehr interessant :ugly: War Notch also auch nicht so intelligent am Anfang.

Bei Version 0.0.14a hat er scheinbar mal das blockige weggelassen. Aber da sieh man, das die Koordinaten jedes Vertecis irgendwo gespeichert sind.

eMo
19.01.2014, 23:43:13
Sagte ja, dass die Glättung anhand der schon vorhandenen Chunks entsteht. Sagen wir du nimmst eine einfache Glättung aus dem Mittel von 24 umherliegenden Voxeln (jeweils der höchste Voxel, der nicht vom Blocktyp Luft ist), so erhälst du am ende ein Chunk, der zu seinen Nachbarn passt ;)
Deswegen erstellst du ja zuerst für jeden Chunk eine eigene Noisemap, am besten lässt du die auch schonmal glätten, jedoch nur über die nächsten Nachbarn, und lässt den Chunk dann erst mit den Nachbarn glätten ;)

eXi
20.01.2014, 00:00:38
Magst du mir vielleicht morgen einmal genauer darlegen was die Glättung ist. Also ich weiß was du damit meinst, aber Coding technisch kann ich mir das grad nicht vorstellen.
Ich werde morgen mal fix ein Voxel und ein Chunk Script schreiben, welches Luft, Erde und Stein als Type hat und dann den Chunk per Noise aufbaut. Dann guck ich mal wie ich darauf das Glätten anwenden kann.

eMo
20.01.2014, 00:33:24
Nunja. Ist ein wenig schwierig zu erklären.


(x-2,y-2)
(x-1,y-2)
(x,y-2)
(x+1,y-2)
(x+2,y-2)


(x-2,y-1)
(x-1,y-1)
(x,y-1)
(x+1,y-1)
(x+2,y-1)


(x-2,y)
(x-1,y)
(x,y)
(x+1,y)
(x+2,y)


(x-2,y+1)
(x-1,y+1)
(x,y+1)
(x+1,y+1)
(x+2,y+1)


(x-2,y+2)
(x-1,y+2)
(x,y+2)
(x+1,y+2)
(x+2,y+2)


Du willst den geglätteten Wert der roten Koordinate (die z-Koordinate soll angepasst werden), also nimmst du die z + die Summe über alle 24 umliegenden z-Koordinaten von (x-2,y-2) bis (x+2,y+2) und teilst sie durch die Anzahl der Felder (25). Du erhälst einen geglätteten Wert, der im Mittel der anderen z-Koordinaten liegt.

eXi
20.01.2014, 00:39:54
Ok, ich versuch das morgen mal anzuwenden! Danke dir soweit (:

eMo
20.01.2014, 00:43:01
Kein Ding, helfe gerne, soweit ich es kann :)

Michael Z.
20.01.2014, 05:26:31
Ich habe nicht alles zu 100% gelesen, will aber was zum Aufbau vor allem zur Persistenz meine Meinung abgeben.

Warum gibt es einen Seed und weshalb sieht eine Welt identisch aus wenn der Seed identisch ist?

Antwort in einem Wort: Pseudo Zufallszahlen

Wird auch in der Cryptografie eingesetzt. Über den Seed generiert ein Generator anscheinlich zufällige Zahlen die möglichst den ganzen Zahlenraum abdecken. Dadurch wird komplett alles berechnet. Es gibt keine wirklich zufällige zahl da sonst das Aussehen der World abweichen würde. Dadurch das diese "zufällige" Zahlen immer identisch ausgespuckt werden (wenn die Werte entsprechend stimmen) kann man darauf aufbauen. Das ist besonders Praktisch wenn weitere Chunks generiert werden sollen. Erst die Performance zwingt ja ein Spiel die Welt in kleinere Happen zu teilen damit alles noch in Echtzeit angezeigt werden kann. Wenn man den Ende eines vorhandenen chunks hat, kann man direkt mit den gespeicherten Werten weiter arbeiten. Grob sollte man sich das wie ein Offset vorstellen können wo der Generator seine Arbeit wieder aufnimmt und zwar mit den Daten die er zuletzt genutzt hat.

Eine Glättung kann ich mir so nicht vorstellen aus dem einfachen Grund: Wenn der Client auf maximalen Radius gestellt hat dann sieht er den Rand der generierten Flächen. Diese müsste sich, wenn man sich der Chunks nähert, angeglichen werden je nachdem wie der nächste Chunk aussieht? Unwarscheinlich und auch unnötig.

Es ist klar das solch ein Prozess nicht einfach ist. Es gibt aber einfache Teilschritte wie z.b. die Verteilung von Rohstoffen in der Erde. Zum Beispiel werden Diamanten zwischen lvl 15 und 0 vorkommen. Eisen von 50 runter. Es muss hier noch festgelegt werden wie stark sich ein Cluster bildet (anzahl der Rohstoffe insgesammt und anzahl der Rohstoffe in der direkten Umgebung), was bei Eisen viel höher liegt als bei Diamanten. Aber auch hier werden wohl konstanten verwendet (sonst, erneut, klappt es nicht mit dem 1:1 prinzip) die über den Pseudo Generator platziert werden.

Was relativ einfach ist ist wohl Sand. Er ist im Grunde überall wo Wasser ist (Meer oder Fluss). Mit einem Festgelegten Bereich (z.b. 3) der mit dem Generator multipliziert wird (der die Position des Units weiß und z.b. 0,5 ausspuckt) = 1,5 = (int)1 Unit nach dem Wasser platziert wird. Clay, wie andere Erze, werden wohl im Nachhinein erzeugt mit der Bedingung das es nur Sand ersetzen kann.

Will man sich selber was zusammenbauen dann sollte es so simpel wie möglich sein: Nur hügelland. Keine Höhlen, Bäume, Wasser (das sollten wohl alles nachträglich hinzugefügte erstellprozesse sein).

eMo
20.01.2014, 11:00:27
Das mit der Glättung siehst du eben falsch. Geglättet wird der generierte Chunk, nicht der vorhandene. ;)
Ich glaube schon, dass sie bei Minecraft auch eine Art Glättung benutzen um Chunks aufeinander abzustimmen.

eXi
20.01.2014, 16:17:21
Das mit der Glättung mach ich später. Sollte ich heute Abend Lust haben (denn gerade bin ich total kaputt), dann werde ich mal ein Voxel und ein Chunk Script schreiben. Dazu ein WorldGenerator der diese aufbaut und mit Blocktypen experimentieren. Also alles erstmal ohne Noise.

Wenn ich den Verlauf von Minecraft mir angucke, hat Notch auch erstmal ohne Noise gearbeitet um die Chunks zu verstehen.

@Michael Z. Das mit dem Seed ist richtig. Zufall gibt es nicht. Der einzige "Zufall" den man mit reinnehmen kann, ist die Zeit in der der PC gestartet wird, bzw hier wenn die Welt generiert wird. Allerdings kann ich dann keinen Seed mehr nehmen und ich will auch weniger eine Zufallsmap, sondern mehr eine durch Unruhe erzeugte Höhe. Ob die zufällig ist oder mit Faktoren exakt nachstelltbar, ist ja egal. Das ist eher sogar gut, wenn Spieler zB die Welt vom Lets Player nachspielen wollen.

eMo
20.01.2014, 17:04:47
Soweit ich weiß, sind Seeds sowas wie Timestamps und in vielen Sprachen kann man damit den Zufallszahlengenerator neu initialisieren ;)

eXi
20.01.2014, 18:05:23
Also bei Minecraft ist es meiner Meinung nach nur eine Zahl (auch wenn ich Buchstaben eintippe, ahoi ASCII Tabelle), welche als Offset, also wirklich nur als Verschiebung genutzt wird. Lass die von mir aus mal 1000 gerechnet sein, damit jede kleine Zahl große Wirkung hat.

eMo
20.01.2014, 18:36:03
Also bei Minecraft ist es meiner Meinung nach nur eine Zahl (auch wenn ich Buchstaben eintippe, ahoi ASCII Tabelle), welche als Offset, also wirklich nur als Verschiebung genutzt wird. Lass die von mir aus mal 1000 gerechnet sein, damit jede kleine Zahl große Wirkung hat.
Wenn der Seed nur Offset wäre, würde das bedeuten, dass die Welt vor Generierung bereits bekannt ist. Ein Indie-Gamestudio will ein MMO mit der Technik rausbringen, irgendwas mit "No Man Sky", bei dem die Welt erforschbar und unbekannt, aber irgendwie vordefiniert ist, Minecraft hat das nicht, also muss der Seed doch die Zufallszahlen beeinflussen ;)

eXi
20.01.2014, 18:39:05
Ja gut. Wir könnten beide mit dem selben Seed starten und so die selbe Welt haben. Was wir für den Seed bekommen, kann man nicht voraussagen. Das ist dann halt ein wenig Zahlendreherei. Aber grob gesagt ist es ein Offset.

eXi
20.01.2014, 21:01:09
Ok, also ich hab mich jetzt mal dran gesetzt. Ich hab allerdings ein wenig nach Tutorial gearbeitet, da ich bei Unity die Funktionen nicht alle zusammen suchen wollte.

Ich hab die Code mal kommentiert (aber auf englisch, sollte wohl fitt gehen bei euch :D).

Momentan erstellt er mir einen 5x5x5 Chunk und setzt den type (0-2) je nach dem auf welcher Höhe j wir sind.
0 = Luft, 1 = Erde, 2 = Stein

War gar nicht mal so leicht die ganzen Seiten von dem Block zu definieren ohne sich andauernd im Kopf zu vertuen. Sah sehr lustig zwischendurch aus.

So sieht das ganze momentan aus:


http://puu.sh/6rZ9s.png


Hier einmal die 4 Skripte:



using UnityEngine;
using System.Collections;

public class WorldGen : MonoBehaviour {

// New chunk

Chunk chunk;

// Use this for initialization
void Start()
{
chunk = new Chunk();
}

void Update()
{
//chunk.Update();
}
}





using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class VoxelMesh
{
// Mesh and material for each voxel

Mesh mesh;
Material mat;

// Lists of points, verticals and triangles to define the voxel

List<Vector3> Points;
List<Vector3> Verts;
List<int> Tris;

// List of UVs to draw a material on the voxel

List<Vector2> UVs;

// Creating the voxel as a GameObect (will be changed later due to limited numbers of GameObjects in Unity)

public GameObject gameobject;

// The half size of a Voxel that has width, height and depth = 1

float size = 0.5f;

// The position of the voxel

Vector3 position;

// Use this for initialization
public VoxelMesh(Vector3 ps, byte type)
{
// Position, comes from the Voxel and the Chunk script to define the position of the new Voxel.
// The position is added on x, y and z

position = ps;

gameobject = new GameObject();

// Creating the Points List and adding the 8 points of the cube clockwise to the list

Points = new List<Vector3>();
Points.Add(new Vector3(position.x - size, position.y + size, position.z - size));
Points.Add(new Vector3(position.x + size, position.y + size, position.z - size));
Points.Add(new Vector3(position.x + size, position.y - size, position.z - size));
Points.Add(new Vector3(position.x - size, position.y - size, position.z - size));

Points.Add(new Vector3(position.x + size, position.y + size, position.z + size));
Points.Add(new Vector3(position.x - size, position.y + size, position.z + size));
Points.Add(new Vector3(position.x - size, position.y - size, position.z + size));
Points.Add(new Vector3(position.x + size, position.y - size, position.z + size));

// Creating the other Lists

Verts = new List<Vector3>();
Tris = new List<int>();
UVs = new List<Vector2>();

// Function to create the mesh. Gets the type to draw the right texture

CreateMesh(type);
}

private void CreateMesh(byte type)
{
// Adding the components that are needed to create the mesh

gameobject.AddComponent("MeshFilter");
gameobject.AddComponent("MeshRenderer");
gameobject.AddComponent("MeshCollider");

// Switch for loading the resources, based on the given type number

switch(type)
{
case 0:
mat = Resources.Load("Material/Air") as Material;
break;
case 1:
mat = Resources.Load("Material/Dirt") as Material;
break;
case 2:
mat = Resources.Load("Material/Stone") as Material;
break;
default:
Debug.LogWarning("Type no available, using default material!");
mat = Resources.Load("Material/Default") as Material;
break;
}

if(mat == null)
{
Debug.LogError("Material not found!");
return;
}

// Getting the Filter component and debug code

MeshFilter meshFilter = gameobject.GetComponent<MeshFilter>();

if(meshFilter == null)
{
Debug.LogError("MeshFilter not found!");
return;
}

// Sets "mesh" to the used mesh and the meshFilter and debug code

mesh = meshFilter.sharedMesh;

if(mesh == null)
{
meshFilter.mesh = new Mesh();
mesh = meshFilter.sharedMesh;
}

// Get the collider of the object and debug code

MeshCollider meshCollider = gameobject.GetComponent<MeshCollider>();

if(meshCollider == null)
{
Debug.LogError("MeshCollider not found!");
return;
}

// Clears the mesh, so that it can be updated again

mesh.Clear();
UpdateMesh();
}

private void UpdateMesh()
{
// Adds the vertecies based on the points to the list

// Front plane
Verts.Add(Points[0]); Verts.Add(Points[1]); Verts.Add(Points[2]); Verts.Add(Points[3]);
// Back plane
Verts.Add(Points[4]); Verts.Add(Points[5]); Verts.Add(Points[6]); Verts.Add(Points[7]);
// Left plane
Verts.Add(Points[5]); Verts.Add(Points[0]); Verts.Add(Points[3]); Verts.Add(Points[6]);
// Right plane
Verts.Add(Points[1]); Verts.Add(Points[4]); Verts.Add(Points[7]); Verts.Add(Points[2]);
// Top plane
Verts.Add(Points[5]); Verts.Add(Points[4]); Verts.Add(Points[1]); Verts.Add(Points[0]);
// Bottom plane
Verts.Add(Points[3]); Verts.Add(Points[2]); Verts.Add(Points[7]); Verts.Add(Points[6]);

// Adds the triangles clockwise to the list

// Front plane
Tris.Add(0); Tris.Add(1); Tris.Add(2);
Tris.Add(2); Tris.Add(3); Tris.Add(0);
// Back plane
Tris.Add(4); Tris.Add(5); Tris.Add(6);
Tris.Add(6); Tris.Add(7); Tris.Add(4);
// Left plane
Tris.Add(8); Tris.Add(9); Tris.Add(10);
Tris.Add(10); Tris.Add(11); Tris.Add(8);
// Right plane
Tris.Add(12); Tris.Add(13); Tris.Add(14);
Tris.Add(14); Tris.Add(15); Tris.Add(12);
// Top plane
Tris.Add(16); Tris.Add(17); Tris.Add(18);
Tris.Add(18); Tris.Add(19); Tris.Add(16);
// Bottom plane
Tris.Add(20); Tris.Add(21); Tris.Add(22);
Tris.Add(22); Tris.Add(23); Tris.Add(20);

// Adds the uv coordinates to the UVs list. It contains the coordinates of the face
// on which the texture will be drawn

// Front plane
UVs.Add(new Vector2(0,1));
UVs.Add(new Vector2(1,1));
UVs.Add(new Vector2(1,0));
UVs.Add(new Vector2(0,0));
// Back plane
UVs.Add(new Vector2(0,1));
UVs.Add(new Vector2(1,1));
UVs.Add(new Vector2(1,0));
UVs.Add(new Vector2(0,0));
// Left plane
UVs.Add(new Vector2(0,1));
UVs.Add(new Vector2(1,1));
UVs.Add(new Vector2(1,0));
UVs.Add(new Vector2(0,0));
// Right plane
UVs.Add(new Vector2(0,1));
UVs.Add(new Vector2(1,1));
UVs.Add(new Vector2(1,0));
UVs.Add(new Vector2(0,0));
// Top plane
UVs.Add(new Vector2(0,1));
UVs.Add(new Vector2(1,1));
UVs.Add(new Vector2(1,0));
UVs.Add(new Vector2(0,0));
// Bottom plane
UVs.Add(new Vector2(0,1));
UVs.Add(new Vector2(1,1));
UVs.Add(new Vector2(1,0));
UVs.Add(new Vector2(0,0));

// Adding the lists to the mesh

mesh.vertices = Verts.ToArray();
mesh.triangles = Tris.ToArray();
mesh.uv = UVs.ToArray();

// Clearing the lists, after they are already stored in the mesh

Verts.Clear();
Tris.Clear();
UVs.Clear();

// Gets the collider component from the gameobject

MeshCollider meshCollider = gameobject.GetComponent<MeshCollider>();

// Refreshs the calculated mesh

mesh.RecalculateNormals();
mesh.RecalculateBounds();

// Function for recalculating the tangents of the mesh

RecalculateTangents(mesh);

// Clears the mesh on the collider and sets it the constructed mesh from above

meshCollider.sharedMesh = null;
meshCollider.sharedMesh = mesh;

// Renders the material on the gameObject renderer

gameobject.renderer.material = mat;

// Some optimization code in background

mesh.Optimize();
}

private static void RecalculateTangents(Mesh mesh)
{
// Arrays for the mesh components

int[] triangles = mesh.triangles;
Vector3[] vertices = mesh.vertices;
Vector2[] uv = mesh.uv;
Vector3[] normals = mesh.normals;

// Length of the triangle and vertex array

int triangleCount = triangles.Length;
int vertexCount = vertices.Length;

// Two arrays of the size of the vertex array

Vector3[] tan1 = new Vector3[vertexCount];
Vector3[] tan2 = new Vector3[vertexCount];

// Not clear at the moment

Vector4[] tangents = new Vector4[vertexCount];

for(long a = 0; a < triangleCount; a += 3)
{
long i1 = triangles[a];
long i2 = triangles[a + 1];
long i3 = triangles[a + 2];

Vector3 v1 = vertices[i1];
Vector3 v2 = vertices[i2];
Vector3 v3 = vertices[i3];

Vector2 w1 = uv[i1];
Vector2 w2 = uv[i2];
Vector2 w3 = uv[i3];

float x1 = v2.x - v1.x;
float x2 = v3.x - v1.x;
float y1 = v2.y - v1.y;
float y2 = v3.y - v1.y;
float z1 = v2.z - v1.z;
float z2 = v3.z - v1.z;

float s1 = w2.x - w1.x;
float s2 = w3.x - w1.x;
float t1 = w2.y - w1.y;
float t2 = w3.y - w1.y;

float div = s1 * t2 - s2 * t1;
float r = div == 0.0f ? 0.0f : 1.0f / div;

Vector3 sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);
Vector3 tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r);

tan1[i1] += sdir;
tan1[i2] += sdir;
tan1[i3] += sdir;

tan2[i1] += tdir;
tan2[i2] += tdir;
tan2[i3] += tdir;
}

for(long a = 0; a < vertexCount; ++a)
{
Vector3 n = normals[a];
Vector3 t = tan1[a];

Vector3.OrthoNormalize(ref n, ref t);
tangents[a].x = t.x;
tangents[a].y = t.y;
tangents[a].z = t.z;

tangents[a].w = (Vector3.Dot(Vector3.Cross(n, t), tan2[a]) < 0.0f) ? -1.0f : 1.0f;
}

// Setting the mesh.tangents to the recalculated tangents

mesh.tangents = tangents;
}
}





using UnityEngine;
using System.Collections;

public class Voxel
{
// Position of the voxel

Vector3 position;

// A mesh from the VoxelMesh class

VoxelMesh mesh;

// The type for the block type

private byte Type;

// getter/setter function for other function to access this variable

public byte type
{
get { return Type;}
set { Type = value;}
}

// Creating the voxelMesh, based on the position and the type provided by the chunk function

public Voxel(Vector3 pos, byte t)
{
position = pos;
type = t;
mesh = new VoxelMesh(position, type);
}

// Not important at the moment (used for setting the air blocks to inactive in the
// checkNeighbor function

public void isActive(bool value)
{
mesh.gameobject.SetActive(value);
}

}





using UnityEngine;
using System.Collections;

public class Chunk
{
// The size of the chunk

int width, height, depth;

// An 3 dimensional array for the voxels

Voxel[,,] voxels;

public Chunk()
{
// Setting the size of the array to the size of the chunk

width = 5;
height = 5;
depth = 5;

voxels = new Voxel[width,height,depth];

// Calling the function that will build the chunk

BuildChunk();
}

// Not important at the moment (used for checking the neighbors of each voxel and
// deactivating them, if they are air blocks)

public void Update()
{
//CheckNeighbors();
}

// Building the chunk of the size width, height and depth

private void BuildChunk()
{
for(int i = 0; i < width; i++)
{
for(int j = 0; j < height; j++)
{
for(int k = 0; k < depth; k++)
{
// Getting a random type of block between 0 and 3 (so 0,1 or 2)
// which is used for the block type 0 = air, 1 = dirt, 2 = stone

//byte type = (byte)Random.Range(0,3);
byte type = 0;

// Testing the types: Using the current height of the building chunk
// to set the type of the block

if(j < 3)
type = 2;
if(j == 3)
type = 1;
if(j == 4)
type = 0;

// Creating the Voxel only if the type is not 0 = air

if(type != 0)
voxels[i,j,k] = new Voxel(new Vector3(i,j,k), type);
}
}
}

//CheckNeighbors();
}

// Not important at the moment (used for checking the neighbors of each voxel and
// deactivating them, if they are air blocks)
}



Ganz am Ende vom Chunk Script hab ich was riesig langes zum Überprüfen der Voxelnachbarn (daher auch die CheckNeighbors() Funktion) mal raus geschnitten, da dies gerade unnütz ist und eh auskommentiert war.

Ich weiß leider jetzt gerade nicht, ob das ein guter Start ist. Die erstellen GameObjects muss ich noch raus machen, weil Unity eine Begrenzung hat. Weiß gerade nicht wie sich das auswirken wird. Hinterher sollten nur noch die Chunks aus GameObject existieren.

Irgendwelche Ideen wie man weiter machen könnte oder was man abändern muss, damit das Fortfahren einfacher wird.

Fortfahren im Sinne von, mehrere Chunks und Noise.

EDIT: Werde das morgen wohl nochmal um/neuschreiben. War eine schöne Übung um das zu verstehen, aber damit kann ich irgendwie nicht arbeiten.

eXi
22.01.2014, 23:36:23
So, hab es neu gescripted. Nachdem ich gerafft hab, wie die Voxel funktionieren, hab ich diese nur noch als Chunks erstellt.

Die Chunks sind momentan 16x128x16.
Die erste Schicht ist nicht abbaubar.
Die ersten 10 Schichten sind Stein mit vereinzelt Erde (einfach nur von einer Randomzahl abhängig gemacht).
Die zweiten 10 Schichten sind Erde.
Danach nur "Luft". Allerdings nicht als Blocktyp sondern einfach nicht gerendert.

Steuerung:

- WASD Laufen, Maus gucken
- Linksklick Block entfernen
- Rechtsklick ausgewählten Block setzen
- Mausrad Blöcke wechseln. Momentan 4 verschiedene. Nummer auf dem Bildschirm zeigt Blocktyp an. Linksklick nutzt Block "0" also "Luft" für das entfernen
- ESC beenden

Lagt noch, da ich keine Ahnung hab wie man das optimieren kann.
Chunks werden momentan nur erstellt und gelöscht, nicht gespeichert und geladen.
Falls man mal durch Blöcke fällt liegt das am Collider vom Spieler. Hab da nur einen vorhandenen genommen. Auch das gucken durch Blöcke ist einfach nur nicht geändert. Geht gerade nur um die Chunks.
Als nächstes käme die Noise einfügen. Hab das mal probiert, aber noch nichts dolles bei raus gekommen.

Hier die Scripts:



using UnityEngine;
using System.Collections;

public class World : MonoBehaviour {



// Use this for initialization
void Start ()
{

}

// Update is called once per frame
void Update ()
{

}

// Function to set a block a the position of the giving type "id"

public static void SetBlock(int id, Vector3 pos)
{
int x = (int) Mathf.Round(pos.x);
int y = (int) Mathf.Round(pos.y);
int z = (int) Mathf.Round(pos.z);

GameObject chunk = GetChunk(pos);

// If the chunk is not empty and the pos.y is not over 128, than we
// are allowed to set a new block

if(chunk)
{
if(pos.y < 128 && pos.y != 0)
{
// Set the x,y,z to the pos of the new block and change the
// Block in the chunk Arraw at position x,y,z to the block
// of type "id"
x = x - (int)chunk.transform.position.x;
y = y - (int)chunk.transform.position.y;
z = z - (int)chunk.transform.position.z;

chunk.GetComponent<ChunkRenderer>().chunk[x,y,z] = id;

chunk.GetComponent<ChunkRenderer>().reRender = true;
}
}
}

// Function to "get" a chunk. We use a sphere collider to see if there are any
// chunks in around the given position and return the founded colliders. Also
// we return "null" if we find nothing

public static GameObject GetChunk(Vector3 pos)
{
pos.x = (int)pos.x;
pos.y = (int)pos.y;
pos.z = (int)pos.z;

Collider[] chunksInPos = Physics.OverlapSphere(new Vector3(pos.x, 0, pos.z), 1);

foreach(Collider hit in chunksInPos)
{
for(int x = 0; x < 16; x++)
{
for(int z = 0; z < 16; z++)
{
if(hit.collider.gameObject.tag == "Chunk")
{
if(hit.collider.gameObject.transform.position + new Vector3(x,0,z) == new Vector3(pos.x,0,pos.z))
{
return hit.collider.gameObject;
break;
}
}
}
}
}

return null;
}

}





using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ChunkRenderer : MonoBehaviour {

// This is the actual chunk of the size 16x128x16. 128 is the heigth

public int[,,] chunk = new int[16,128,16];

// Used for generating the chunk

public int chunkSizeX = 16;
public int chunkSizeY = 128;
public int chunkSizeZ = 16;

// Used for the chunk position

public int chunkPositionX;
public int chunkPositionY;
public int chunkPositionZ;

// Reference to the player to destroy or generate chunks

public GameObject player;

// Used for rerendering the chunk and generate new chunks

public bool reRender = false;
public bool done = false;
public bool LastChunk = false;

// More Variable for trees etc.

// Use this for initialization
void Start ()
{
// Finding the player in the game

player = GameObject.Find("Player");

// Used for displaying the position in the name of the gameobject

chunkPositionX = (int)transform.position.x;
chunkPositionY = (int)transform.position.y;
chunkPositionZ = (int)transform.position.z;

this.name = "Chunk(" + chunkPositionX.ToString() + "," + chunkPositionY.ToString() + "," + chunkPositionZ.ToString() + ")";

// Filling the chunks with blocks. Later we are gonna change the id (atm 1) to the specific type of blocks

for(int x = 0; x < chunkSizeX; x++)
{
for(int y = 0; y < chunkSizeY; y++)
{
for(int z = 0; z < chunkSizeZ; z++)
{
int value = Random.Range(0, 200);
if(y == 0)
chunk[x,y,z] = 1;
if(y > 0 && y < 10)
{
if(value < 20)
chunk[x,y,z] = 2;
if(value >= 20)
chunk[x,y,z] = 1;
}
if(y >= 10 && y < 20)
chunk[x,y,z] = 2;
if(y >= 20)
chunk[x,y,z] = 0;

}
}
}

// Several other typs of blocks, for example trees



// Adding the MeshCollider for Colliding

if(!gameObject.GetComponent<MeshCollider>())
{
gameObject.AddComponent("MeshCollider");
}

ChunkRender(chunk);
}

// Update is called once per frame
void Update ()
{
// Used for rerendering the chunk if for example a block was added or removed

if(reRender == true)
{
ChunkRender(chunk);
reRender = false;
}

// If the player is 16 units away from the chunk, destroy it. Later we will save the chunk
// And reload it if the player comes near it

if(Vector3.Distance(transform.position, player.transform.position) > 128)
{
Destroy(this.gameObject);
}

// If this chunk is the last Chunk (so he has chunks missing around him), we will add new chunks near him

if(LastChunk == true)
{
if(Vector3.Distance(transform.position, player.transform.position) < 64)
{
int Radius = 1;

for(int x = -Radius; x <= Radius; x++)
{
for(int z = -Radius; z <= Radius; z++)
{
// So if theres no chunk returned at this position, we gonna instatiate a new one

if(World.GetChunk(transform.position + new Vector3(x * 16,0,z *16)) == null)
{

Debug.Log("MORE CHUNKS!");
GameObject newChunk = Instantiate(this, transform.position + new Vector3(x * 16,0,z * 16), Quaternion.identity) as GameObject;

// Set LastChunk at this chunk to false and LastChunk to true at the new chunk

if(x == Radius || x == -Radius || z == Radius || z == -Radius)
{
if(newChunk)
{
LastChunk = false;
newChunk.GetComponent<ChunkRenderer>().LastChunk = true;
}
}
}
}
}
}
}
}

// Now the actual render progress for the chunk

Mesh ChunkRender(int[,,] chunk)
{
Mesh mesh = GetComponent<MeshFilter>().mesh;

if(mesh == null)
{
Debug.Log ("No Meshfilter found!");
}

// Lists for the mesh things

List<Vector3> vertices = new List<Vector3>();
List<Vector2> uvs = new List<Vector2>();
List<int> triangles = new List<int>();
List<int>[] indices = new List<int>[0];

int vertexIndex;

// Used to prevent out of bounce error

int top;
int bottom;
int front;
int back;
int left;
int right;

vertexIndex = 0;

// Clear the mesh to only have the new mesh in it

mesh.Clear();

for(int x = 0; x < chunkSizeX; x++)
{
for(int y = 0; y < chunkSizeY; y++)
{
for(int z = 0; z < chunkSizeZ; z++)
{
int block = chunk[x,y,z];

if(y == chunkSizeY - 1) { top = 0; } else { top = chunk[x,y + 1,z]; }
if(y == 0) { bottom = 0; } else { bottom = chunk[x,y - 1,z]; }
if(z == chunkSizeZ - 1) { right = 0; } else { right = chunk[x,y,z + 1]; }
if(z == 0) { left = 0; } else { left = chunk[x,y,z - 1]; }
if(x == chunkSizeX - 1) { front = 0; } else { front = chunk[x + 1,y,z]; }
if(x == 0) { back = 0; } else { back = chunk[x - 1,y,z]; }

int scale = renderer.material.mainTexture.width / 1024;

float StartTexturePosition = -0.5F / scale + (float)block / 2 / scale + 0.005F;
float EndTexturePosition = StartTexturePosition + 0.5F / scale - 0.005F;

if(block > 0 && top == 0)
{
// Add vertices and triangles

vertices.Add(new Vector3(x, y + 1, z));
vertices.Add(new Vector3(x, y + 1, z + 1));
vertices.Add(new Vector3(x + 1, y + 1, z + 1));
vertices.Add(new Vector3(x + 1, y + 1, z));

triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex);

// Add uvs

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex += 4;
}

if(block > 0 && bottom == 0)
{
// Add vertices and triangles

vertices.Add(new Vector3(x, y, z));
vertices.Add(new Vector3(x + 1, y, z));
vertices.Add(new Vector3(x + 1, y, z + 1));
vertices.Add(new Vector3(x, y, z + 1));

triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex);

// Add uvs

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex += 4;
}

if(block > 0 && right == 0)
{
// Add vertices and triangles

vertices.Add(new Vector3(x, y, z + 1));
vertices.Add(new Vector3(x + 1, y, z + 1));
vertices.Add(new Vector3(x + 1, y + 1, z + 1));
vertices.Add(new Vector3(x, y + 1, z + 1));

triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex);

// Add uvs

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex += 4;
}

if(block > 0 && left == 0)
{
// Add vertices and triangles

vertices.Add(new Vector3(x, y, z));
vertices.Add(new Vector3(x, y + 1, z));
vertices.Add(new Vector3(x + 1, y + 1, z));
vertices.Add(new Vector3(x + 1, y, z));

triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex);

// Add uvs

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex += 4;
}

if(block > 0 && front == 0)
{
// Add vertices and triangles

vertices.Add(new Vector3(x + 1, y, z));
vertices.Add(new Vector3(x + 1, y + 1, z));
vertices.Add(new Vector3(x + 1, y + 1, z + 1));
vertices.Add(new Vector3(x + 1, y, z + 1));

triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex);

// Add uvs

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex += 4;
}

if(block > 0 && back == 0)
{
// Add vertices and triangles

vertices.Add(new Vector3(x, y, z + 1));
vertices.Add(new Vector3(x, y + 1, z + 1));
vertices.Add(new Vector3(x, y + 1, z));
vertices.Add(new Vector3(x, y, z));

triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex);

// Add uvs

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex += 4;
}
}
}
}

// Add the vertices, triangles and uvs to the mesh

mesh.vertices = vertices.ToArray();
mesh.triangles = triangles.ToArray();
mesh.uv = uvs.ToArray();

// Recalculate it after that

mesh.RecalculateNormals();
mesh.RecalculateBounds();

// Set the mesh of the MeshCollider to the new mesh, but first clear it to be
// sure that only the new mesh is in it

GetComponent<MeshCollider>().sharedMesh = null;
GetComponent<MeshCollider>().sharedMesh = mesh;

return mesh;
}
}





using UnityEngine;
using System.Collections;

public class ChunkGenerator : MonoBehaviour {

public GameObject chunk;

public int PrevWorldSize;
public int WorldSize;

// Use this for initialization
void Start ()
{
PrevWorldSize = WorldSize;
GenerateChunks();
}

// Update is called once per frame
void Update ()
{

}

void GenerateChunks()
{
PrevWorldSize = WorldSize;

for(int x = -WorldSize; x <= WorldSize; x++)
{
for(int z = -WorldSize; z <= WorldSize; z++)
{
if(World.GetChunk(new Vector3(x * 16,0,z * 16)) == null)
{
Debug.Log("Chunk generated!");
GameObject newChunk = Instantiate(chunk, new Vector3(x * 16,0,z * 16), Quaternion.identity) as GameObject;
if(x == WorldSize || x == -WorldSize || z == WorldSize || z == -WorldSize)
{
newChunk.GetComponent<ChunkRenderer>().LastChunk = true;
}
}
}
}
}
}





using UnityEngine;
using System.Collections;

public class PlayerScript : MonoBehaviour {

public int currentBlockID;

public Vector3 position;

// Use this for initialization
void Start ()
{
Screen.lockCursor = true;
}

// Update is called once per frame
void Update ()
{
Screen.showCursor = true;

ChooseBlockType();
ChangeBlock();

if(Input.GetKey(KeyCode.Escape))
{
Application.Quit();
}
}

void ChooseBlockType()
{
if(Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetAxis("Mouse ScrollWheel") > 0.0F || Input.GetAxis("Mouse ScrollWheel") < 0.0F)
{
Screen.lockCursor = true;
}

if(Input.GetAxis("Mouse ScrollWheel") > 0.0F)
{
if(currentBlockID > 1)
{
currentBlockID -= 1;
}
else
{
currentBlockID = 8;
}
}


if(Input.GetAxis("Mouse ScrollWheel") < 0.0F)
{
if(currentBlockID < 8)
{
currentBlockID += 1;
}
else
{
currentBlockID = 1;
}
}
}

void ChangeBlock()
{
Ray ray = Camera.mainCamera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2, 0));
RaycastHit hit;

if (Physics.Raycast(ray, out hit, 5F))
{
position = new Vector3(Mathf.FloorToInt(hit.point.x - (hit.normal.x / 10)), Mathf.FloorToInt(hit.point.y - (hit.normal.y / 10)), Mathf.FloorToInt(hit.point.z - (hit.normal.z / 10)));

position += hit.normal;

if (Input.GetMouseButtonDown(1))
{
if(Vector3.Distance(transform.position, position) > 0.5F)
{
World.SetBlock(currentBlockID, position);
}
}

if (Input.GetMouseButtonDown(0))
{
position -= hit.normal;
World.SetBlock(0, position);
}
}
}

void OnGUI ()
{
GUI.Label(new Rect(Screen.width / 2 - 100,Screen.height / 2 - 299, 200, 20), currentBlockID.ToString());
}
}



Und hier das "Spiel".

https://www.dropbox.com/s/w51bghs535mgstg/VoxelEngineTest.rar

Falls das überhaupt noch jemanden interessiert :ugly:

Nilo
22.01.2014, 23:58:46
Hört sich ja schon deutlich nach Fortschritt an :daumen:
Würde es gerne testen, aber ich bekommen folgende Fehlermeldung:
There should be 'VoxelEngineTest_Data'
folder next to the executable

eXi
23.01.2014, 00:02:51
Ach, stimmt warte. Link wird geupdatet.

Oben geupdated. Hier nochmal: https://www.dropbox.com/s/w51bghs535mgstg/VoxelEngineTest.rar

eXi
24.01.2014, 16:54:25
Hab jetzt ein wenig hin und her überlegt. Hat jemand eine Idee wie man anders förmige Blöcke, wie zB einen Zaun oder eine Treppe implementieren kann? Muss ich dafür jedes mal die Punkte und Faces so berechnen wie sie hinterher aussehen sollen? o.o Das ist ja voll viel Arbeit.

eMo
24.01.2014, 17:07:06
Du könntest natürlich in deinem Blocktype das Aussehen des Block definieren und hättest dann kaum noch Arbeit ;)
Dann solltest du aber eine Methode schreiben, die die gleichartigen Nachbarn und ihre Positionen bestimmt, damit du Treppen um die Ecke bauen kannst, etc

eXi
24.01.2014, 17:31:11
Naja momentan ist der Blocktyp nur eine Nummer, welche im Chunk Array steht und die Texture auf der großen Gesamttextur richtig skalliert.

Mr Lambda
24.01.2014, 17:38:49
Könntest du noch eine schicke Sonne einbauen, die Schatten wirft? :)

eXi
24.01.2014, 18:08:09
Öhm, ja klar. Hab beim Licht die Schatten nur nicht angehabt, weil mir das grad egal war.
Kann gerne eine neue Version hochladen, wenn ich noch etwas mehr geändert habe.

eMo
24.01.2014, 19:25:46
Naja momentan ist der Blocktyp nur eine Nummer, welche im Chunk Array steht und die Texture auf der großen Gesamttextur richtig skalliert.

Und dann hast du da schon deinen "Fehler".
Warum ist dein Chunk kein Objekt-Array in dem der Block gespeichert ist und du renderst einfach nur die Blöcke, die mindestens einen Nachbarn vom Typ Block.Air haben?

Ich würde immer so viel wie möglich zur kleinsten Ebene durchreichen. Musst natürlich selbst überlegen, inwiefern das sind macht, immer tiefer zu gehen, aber ich glaube, momentan wäre das so einfacher ;)

Habe aber natürlich keine Ahnung, wie wo was und warum Unity wie wo was und warum etwas macht, wie es macht, wenn es ums rendern geht! :ugly:

eXi
24.01.2014, 21:47:58
Also, wenn ich die Voxels bzw Blöcke als Objekte erstelle, dann wird das zu viel. Ich kann nur eine bestimmte Menge an Objekten in Unity speichern. Deshalb sind die Chunks schon gut. Ich kann den Blocktyp jederzeit überprüfen wenn man ihn anklickt.
Ich könnte also zB ein ObjektSpawnScript schreiben, welches zB einen Stein ausgibt, wenn die geschlagene Chunk Zelle eine 1 hat (also Stein ist). Ich rendere momentan schon nur die äußeren Seiten. Das ist das minimalste was geht.

Ich hab jetzt mal einen schmalen Block für ID 4 gemacht. Das hab ich wie folgt umgesetzt:


if(block == 4)
{
// Add vertices and triangles


// Top
if(chunk[x,y + 1,z] == 0)
{
vertices.Add(new Vector3(x + 0.25f, y + 1, z + 0.25f));
vertices.Add(new Vector3(x + 0.25f, y + 1, z + 0.75f));
vertices.Add(new Vector3(x + 0.75f, y + 1, z + 0.75f));
vertices.Add(new Vector3(x + 0.75f, y + 1, z + 0.25f));

triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex);

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex +=4;

}
// Bottom
if(chunk[x,y - 1,z] == 0)
{
vertices.Add(new Vector3(x + 0.25f, y, z + 0.25f));
vertices.Add(new Vector3(x + 0.25f, y, z + 0.75f));
vertices.Add(new Vector3(x + 0.75f, y, z + 0.75f));
vertices.Add(new Vector3(x + 0.75f, y, z + 0.25f));

triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex);

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));


vertexIndex += 4;

}

// Left
vertices.Add(new Vector3(x + 0.75f, y, z + 0.25f));
vertices.Add(new Vector3(x + 0.75f, y + 1, z + 0.25f));
vertices.Add(new Vector3(x + 0.25f, y + 1, z + 0.25f));
vertices.Add(new Vector3(x + 0.25f, y, z + 0.25f));

triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex);

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex +=4;

// Right
vertices.Add(new Vector3(x + 0.25f, y, z + 0.75f));
vertices.Add(new Vector3(x + 0.25f, y + 1, z + 0.75f));
vertices.Add(new Vector3(x + 0.75f, y + 1, z + 0.75f));
vertices.Add(new Vector3(x + 0.75f, y, z + 0.75f));

triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex);

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex +=4;

// Front
vertices.Add(new Vector3(x + 0.25f, y, z + 0.25f));
vertices.Add(new Vector3(x + 0.25f, y + 1, z + 0.25f));
vertices.Add(new Vector3(x + 0.25f, y + 1, z + 0.75f));
vertices.Add(new Vector3(x + 0.25f, y, z + 0.75f));

triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex);

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex +=4;

// Back
vertices.Add(new Vector3(x + 0.75f, y, z + 0.25f));
vertices.Add(new Vector3(x + 0.75f, y + 1, z + 0.25f));
vertices.Add(new Vector3(x + 0.75f, y + 1, z + 0.75f));
vertices.Add(new Vector3(x + 0.75f, y, z + 0.75f));

triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 2);
triangles.Add(vertexIndex + 3);
triangles.Add(vertexIndex);

uvs.Add(new Vector2(StartTexturePosition, StartTexturePosition));
uvs.Add(new Vector2(StartTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, EndTexturePosition));
uvs.Add(new Vector2(EndTexturePosition, StartTexturePosition));

vertexIndex +=4;


}


Da drin kann ich auch noch überprüfen um der Block links, rechts, vorne oder hinten auch so ein Block 4 ist und eine Verbindung rendern. Also wie bei einem Zaun in Minecraft.

Ich kann halt nur nicht direkt ein Objekt beim abbauen ansprechen und diesem Schaden zufügen. Also wenn das abbauen etwas dauern soll. Da müsste ich dann drum rum arbeiten. Allerdings weiß ich noch nicht wie :/

Hier mal die überarbeitete Version mit schmalem Block (Mausrad bis Nummer 4 drehen) und Schatten aktiviert (am Anfang auf "Fantastic" stellen, sonst siehts hässlich aus :ugly:).
Außerdem werden die Chunks nun gespeichert wenn sie noch nicht gespeichert sind und geladen wenn sie schon vorhanden sind.
Momentan geht das wie folgt (hab mir keine Gedanken über Performance gemacht).

Es wird der Ordner "world" angelegt (später durch Eingabe vom Nutzer um unterschiedliche Welten zu erstellen). Wenn ein Chunk erstellt wird, dann wird sein Name, welcher die Position enthält, sowie seine Position und sein chunk selber an den ChunkManager übergeben. Dieser schreibt den Chunk in eine Textdatei mit dem Namen des Chunks. Dort steht die Position (welche momentan nicht verwendet wird, da sie schon im Dateinamen steht) und das komplette Chunkarray drin.

Der Chunk überprüft am Anfang selber, ob eine Datei mit seinem Namen existiert. Ist dies der Fall, lädt der ChunkManager die Datei und übergibt die Daten an den Chunk, welcher sich dann neu rendert. Jedes mal wenn er sich neu rendert, speichert er sich auch ab. Das müsste man später ändern, damit nicht bei jedem Block gespeichert wird, aber da geht es mir gerade nicht drum.



using UnityEngine;
using System.Collections;
using System.IO;
using System.Text.RegularExpressions;

public class ChunkManagerScript : MonoBehaviour {

Vector3 position;

// Use this for initialization
void Start ()
{

}

// Update is called once per frame
void Update () {

}

public void saveChunk(Vector3 pos, int[,,] chunk, string fileName)
{
if(!System.IO.Directory.Exists("world"))
{
System.IO.Directory.CreateDirectory("world");
}

System.IO.StreamWriter file = new System.IO.StreamWriter("world/" + fileName + ".txt");
file.Write("position=" + pos.x + "," + pos.y + "," + pos.z + "\r\n");
file.Write ("chunkData=");

for(int x = 0; x < 16; x++)
{
for(int y = 0; y < 128; y++)
{
for(int z = 0; z < 16; z++)
{
file.Write(chunk[x,y,z].ToString() + ",");
}
}
}


file.Close();
}

public string[] loadChunk(Vector3 pos, string fileName)
{
string chunkData = System.IO.File.ReadAllText("world/" + fileName + ".txt");

string[] chunkDataLine = Regex.Split(chunkData, "\r\n");

foreach(string line in chunkDataLine)
{
string[] thisLine = Regex.Split(line, "=");

if(thisLine[0] == "position")
{
string[] positions = Regex.Split(thisLine[1], ",");
Vector3 position;
position.x = int.Parse(positions[0]);
position.y = int.Parse(positions[1]);
position.z = int.Parse(positions[2]);
Debug.Log("DataLoaded");
Debug.Log(position);
}

if(thisLine[0] == "chunkData")
{
string[] blocksString = Regex.Split(thisLine[1], ",");

return blocksString;
}
}
return null;
}
}






if(System.IO.File.Exists("world/" + this.name + ".txt"))
{
loadChunkData = true;
}
else
{
saveChunkData = true;
}


chunkManager = GameObject.Find("ChunkManager");
ChunkManagerScript chunkScript = chunkManager.GetComponent<ChunkManagerScript>();

if(loadChunkData == true)
{
string[] chunkData = chunkScript.loadChunk(chunkPosition, this.name);

Debug.Log(chunkData[0]);
int i = 0;
for(int x = 0; x < 16; x++)
{
for(int y = 0; y < 128; y++)
{
for(int z = 0; z < 16; z++)
{
chunk[x,y,z] = int.Parse(chunkData[i]);
i++;
}
}
}

reRender = true;

loadChunkData = false;
}

if(saveChunkData == true)
{
chunkScript.saveChunk(chunkPosition, chunk, this.name);

saveChunkData = false;
}


https://www.dropbox.com/s/g4wwmrpy60tckwe/VoxelEngineTestv0.1.rar

Mr Lambda
24.01.2014, 22:02:00
Schatten aktiviert (am Anfang auf "Fantastic" stellen, sonst siehts hässlich aus ).

Seh keine :(

€:Liegt sicher an der frühen Version, aber das generieren muesste auf jedenfall optimiert werden, am Anfang gibts bei erstmal nur ein Standbild, und dann sobald ich mich so Bewege das was generiert wird, stockt es kurz.

eXi
24.01.2014, 22:09:32
Sorry, mir war wegen einer endlos Schleife Unity abgestürzt. Hatte nicht gemerkt, dass das nicht gespeichert wurde. Der Lag am Anfang ist übrigens normal. Das wird bei Minecraft mit dem Ladebildschirm vertuscht :D

Hier die das sollte nun mit Schatten sein :X

https://www.dropbox.com/s/92b7ey74dz7e2ln/VoxelEngineTextv0.1.1.rar

Sind natürlich erstmal nur Schatten. Könnte eigentlich mal die Skybox anmachen x) aber Tatsache ist, das ist gerade gar nicht wichtig.

Mr Lambda
24.01.2014, 22:37:01
Ich war mal so Frei und habe einen Target Render gebastelt. :ugly:

http://farm3.staticflickr.com/2869/12123465725_3c92aa8a7c.jpg (http://www.flickr.com/photos/witcher/12123465725/)
Ohne Titel 1 (http://www.flickr.com/photos/witcher/12123465725/) von T.Schlicht (http://www.flickr.com/people/witcher/) auf Flickr

Kann Unity Ambient Occlusion und Global Illumation?:)

Und zu den Schatten kann Unity auch einfach nur Raytraced Schatten, also einfach mit harten Kanten, wie sie z.B. auch in Doom 3/F.E.A.R. vorkommen? Diese Gefilterten oder was das sein sollen sehen naja aus.:ugly:

Ich würde es ziehmlich edel finden, wenn das Projekt dem Bild nahe kommt.:>

eXi
24.01.2014, 23:00:53
SSAO nur mit Pro Version glaube ich. Global Illumination müsste gehen. Gab es mal ein Free Projekt für. Bin leider nicht so versiert in Sachen Licht und Schatten.

Meinst du das ungefähr so? Wie gesagt, SSAO gibs leider in der Free Version nicht.

EDIT: Warte eben nochmal neu.

EDIT2: So, hab AA mal höher gestellt für Fantastic:

http://puu.sh/6wMWn.jpg


EDIT3: Du hast bei dir in der Szene aber mehr als ein Licht oder? Hab bei mir grad nur eins für die Sonne.

EDIT4: Muss die Schatten leider auf maximum lassen, sonst hab ich unter der Erde keine Dunkelheit :X Bin ja nur Free Versions benutzer. Mir geht es gerade auch mehr um den Code als um die Schatten und das Licht.

Hab aber mal für unter der Erde per Tastendruck "T" ein Licht spawnen lassen. Leider casten die keine Schatten in der Free Version -_-

eMo
25.01.2014, 00:58:45
Kann Unity denn wenigstens Enumerations? Die sind ja statisch fest und du könntest trotzdem einen Renderer für sie implementieren und du könntest auch verständlichen Code produzieren, wenn man nicht 1-125691236 als Block hat, sondern normale Namen.
Ich frage mich tatsächlich, wie Minecraft das macht. Ob die verschiedene Objekte haben, die alle auf einem Interface basieren oder sowas.

Oder alternativ:
Du baust eine Enumeration für den Blocktyp (kannst es natürlich auch bei Zahlen belassen, aber imo Enum > Zahl) und eine Block-Render-Klasse, der du einen Vektor und deinen Chunk übergibst und die dann anhand der Enum den zu rendernden Block zurück gibt.
Ich habe das Gefühl, ich denke zu komplex und versuche das ganze unnötig aufzubauschen :ugly:

eXi
25.01.2014, 01:14:14
Das Problem ist einfach, sobald ich den Block als Objekt anlege, hab ich pro Chunk 16*128*16 GameObjects. Das sind zu viele für Unity. Den Chunk immer neu zu rendern, wenn man einen Wert da drin abändert macht bei mir grad noch am meisten Sinn.

Ich finde es halt nur komisch, das ich für jeden Block, der anders aussieht im Chunk, eine neuen Mesh bauen muss.

Was meinst du mit verständlichem Code? Jedes Feld im 3 Dimensionalen Chunk hat eine Nummer. Die steht für den Block. Da Minecraft auch Blocknummern hat, denke ich das dort ungefähr genauso gearbeitet wird.
Egal welches Tutorial ich gucke über Voxel Engines, es wird dort ungefähr so gemacht.

Ich habe allerdings gerade ein ganz anderes Problem. Wenn ich am Rand von einem Chunk bin und den Block abfrage der im nächsten Chunk liegen würde, krieg ich OutOfBounds (was logisch ist, weil dort nichts mehr ist im aktuellen Chunk). Also muss ich sobald das passiert den Block des anliegenden Chunks prüfen -_- ARGH das ist soviel Code, das kommt mir komisch vor.

eMo
25.01.2014, 11:36:52
Eine Enum ist ein statisches Objekt ähnlich einer Zahl und daher denke ich nicht, dass in Minecraft mit Zahlen gearbeitet wird :D
Wie gesagt, wenn Unity das nicht können sollte, sind die Zahlen völlig legitim, dann würde ich aber trotzdem noch ein BlockRenderer schreiben, der dir je nach Blocktyp den gerenderten Block zurückgibt. Du fragst vorher im Chunk ab, welche Blöcke überhaupt gerendert werden, nämlich die, die einen Nachbarn Luft oder anderen Spezialblock haben, und gibst diese dann an deinen BlockRenderer weiter. Der gibt dir einen gerenderten Block oder sowas zurück, den du mit dem ChunkRenderer dann zu einem Gesamtbild zusammenfügst ;)

eXi
25.01.2014, 11:50:42
Ja ok, so ähnlich mach ich das ja schon. Wenn ich Luft neben mir hab, werden die äußeren Faces gerendert. Also die zur Luft hin. Er kann Enum, aber ich hab damit noch nie gearbeitet glaube ich. Nur für Coroutines. Muss ich mir mal angucken.

Aber solange ich für jeden Block einen Mesh "schreiben" muss, also seine Punkte etc definieren muss, nimmt mir das eigentlich keine Arbeit ab.

Hab de Berechnung der Triangles etc jetzt für jeden Block in ein extra Script geschoben, welches nur angefragt wird, wenn der Block Typ die jeweilige Nummer hat. Da könnte ich dann auch die UVs und die Statspoints glaube ich abarbeiten. >.< boah wenn man keinen Plan hat, wie man das sinnvoll aufbaut, dann ist das echt Resourcenfressend. Hut ab für Notch.

Ich verstehe noch nicht, wenn ich einen einzigen Block mit einer Spitzhacke abarbeite, kriegt der ja in Minecraft Schaden und ändert seine Texture. Aber wie mach ich das für jeden Block? Dafür brauche ich doch jeweils eine eigene Instanz vom Block. Oder geht das mit Enums und ich sollte mir das mal angucken?

eXi
25.01.2014, 20:33:46
So, hab es nochmal überarbeitet und ein paar Berechnungen zu Funktionen zusammengefasst. Dazu noch die UV Verteilung geändert damit jedes Face eine eigene Texture kriegen könnte.
Dann hab ich mal die Noise eingefügt für Berge.

Das ganze ist immer noch ziemlich laggy, da die außen Faces jedes Chucks gerendert werden. Ich kann das zwar ausschalten, aber zum einen sieht man dann bei den Übergängen der Chunks zwischen den Bergen teilweise keine Faces und zum anderen will meine Methode um Blöcke zu setzen nicht mehr.

Hab mal versucht einfach den Nachbarchunk einzufangen und zu gucken, ob der angrenzende Block Luft ist, aber das will auch nicht so wahnsinnig gut klappen.

Naja, mit "T" könnt ihr weiterhin Lichter unter der Erde setzen. Hab jetzt erstmal keine Lust mehr auf Voxels und Chunks. Kopfschmerzen von dem kack.

Wenn noch jemand drüber nachdenken will, wie man meine Probleme lösen kann, kann ich die abgeänderten Scripte gerne nochmal posten.

https://www.dropbox.com/s/a1moml81hs6v574/VoxelEngineTestv0.2.rar

eMo
26.01.2014, 00:26:20
Ich hätte das ganze in Java über statische Objekte, die das Interface Block implementieren, realisiert. Dadurch hast du nämlich im Chunk einfach x*y*z Interfaces gespeichert, denn da jeder Blocktyp Block implementiert, castest du das einfach auf Block und kannst dann die Funktion render() vom Interface Block nutzen. Dabei nutzt er dann die Funktion render() des statischen Objektes Blocktyp.
Ich weiß wie gesagt nicht, wie Unity das handhabt, kann dir daher da auch schlecht helfen.
Warum kannst du angrenzende Chunks nicht abfragen? Du lädst sie doch eh? Lade halt 9+12 statt 9 und rendere die 12 neuen nicht.
Ansonsten kennst du doch die Position der Chunks und die des Spielers innerhalb der Chunkwelt und innerhalb des Chunks, wo liegt da das Problem bei der Abfrage des Nachbarn im Nachbarchunk?

eXi
26.01.2014, 01:13:36
Doch ich kann die finden und "geladen" gibt es nicht. Entweder sie sind da oder liegen gespeichert in der Datei. Die Sache ist, wenn ich den zweiten Chunk anfrage und dann den Block, dann muss ich den ersten Chunk auch jedes mal nochmal rendern, weil der zweite Chunk ja auch erst nach dem ersten erstellt wird. Und jedes mal den alten rerendern und die Chunks anfragen laggt glaube ich wie sau. Kann das morgen mal testen.

Zu der Sache mit java. Ich hab mich mit Java nie befasst und bin da auch kein Fan von. :P Deshalb versuche ich das lieber auf C# mit Unity. Ist ja auch nur Rumspielerei weil es nicht so viel Zeit kostet wie Ricochet oder die Tutorials.