public void Build(VoxelCache cache, Biome biome) { // Check if neighbors are loaded for next time if (state == ChunkState.VoxelsLoaded && NeighborsLoaded()) { state = ChunkState.NeighborsLoaded; } // Next, build the mesh if (state == ChunkState.NeighborsLoaded) { if (!isEmpty && !isCompletelySolid) { // Second pass: Add visible voxels FindVisible(cache, biome); // Third pass: Build mesh out of visible voxels BuildMesh(cache, biome); } state = ChunkState.Meshed; // Remove data from cache and reset cache.voxels.Remove(offset); cache.ResetData(); // Set matrix transformation transformMatrix = Matrix.CreateTranslation(offset.X, offset.Y, offset.Z); } }
/// <summary> /// Copy voxel data to 3D array /// </summary> /// <param name="voxels"></param> private void BuildVoxels(VoxelCache cache) { for (int i = 0; i < 256; i++) { byte b = voxelData[i * 3]; byte g = voxelData[i * 3 + 1]; byte r = voxelData[i * 3 + 2]; colors[i] = (uint)((r << 24) | (g << 16) | (b << 8)); } // Add non-empty voxels to the array for (int i = 768; i < voxelData.Length - 6; i += 4) { int x = voxelData[i + 1]; int z = voxelData[i + 2]; int y = voxelData[i + 3]; cache.voxels[Vector3.One][x + 1, z + 1, y + 1] = voxelData[i]; } // Add bounding box extents int offset = voxelData.Length - 6; extents[0] = new Vector3(voxelData[offset], voxelData[offset + 2], voxelData[offset + 1]); extents[1] = new Vector3(voxelData[offset + 3] + 1, voxelData[offset + 5] + 1, voxelData[offset + 4] + 1); }
/// <summary> /// Generate additional 3D objects using the heightmap and surrounding blocks /// </summary> private void GenerateExtraObjects(Biome biome, VoxelCache cache, int x, int z) { int newOffsetX = (int)(offset.X + (x * cache.SizeX)); int newOffsetZ = (int)(offset.Z + (z * cache.SizeZ)); int seed = cache.GetHashCode() ^ ((newOffsetX << 13) + (newOffsetZ >> 5));// ^ cache.MapSeed; Random rnd = new Random(seed); int localX = rnd.Next(cache.SizeX) ^ (mapSeed & 0x1f); int localZ = rnd.Next(cache.SizeZ) ^ (mapSeed & 0x1f); localX++; localZ++; float objectDist = Noise.Simplex.Generate( (float)(((float)newOffsetX / 800f) + 120f), (float)(((float)newOffsetZ / 800f) + 100f)); objectDist = (float)Math.Pow((objectDist * 0.5f) + 0.5f, 1.2f); int objectChance = rnd.Next(100); int chance = (int)(objectDist * 100f) / 2; // We can grab the height value instantly from the noise pattern float height = biome.GetBaseHeight(localX + newOffsetX, localZ + newOffsetZ, false); if (height < 80 && objectChance < chance) { Engine.TreePopulator treePopulator = new Engine.TreePopulator(biome, cache.voxels[offset], rnd); treePopulator.Populate( localX + (x * cache.SizeX), localZ + (z * cache.SizeZ), (int)height, cache.SizeX); } }
/// <summary> /// Second step in voxel mesh generation /// Add visible voxels to the list. /// </summary> private void FindVisible(VoxelCache cache) { // Temporary array for visible voxels byte[, ,] visibleVoxels = new byte[cache.SizeX + 2, cache.SizeZ + 2, cache.SizeY + 2]; for (int y = cache.SizeY - 1; y >= 0; --y) { Parallel.For(0, cache.SizeX, x => { for (int z = 0; z < cache.SizeZ; ++z) { byte voxel = (byte)cache.voxels[Vector3.One][x, z, y]; if (voxel != 0) { int neighbors = 0; if (x == 0 || y == 0 || z == 0 || x == cache.SizeX + 1 || y == cache.SizeY + 1 || z == cache.SizeZ + 1) { visibleVoxels[x, z, y] = voxel; } else { // Immediate side neighbors neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x - 1, z, y] > 0) << NeighborBlock.Middle_W; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x + 1, z, y] > 0) << NeighborBlock.Middle_E; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x, z - 1, y] > 0) << NeighborBlock.Middle_N; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x, z + 1, y] > 0) << NeighborBlock.Middle_S; // Check top and bottom neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x, z, y - 1] > 0) << NeighborBlock.Bottom_Center; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x, z, y + 1] > 0) << NeighborBlock.Top_Center; // Add to the visible list if not occluded on all sides if (neighbors != 0x3f) { visibleVoxels[x, z, y] = voxel; cache.visibleNeighbors[x - 1, z - 1, y - 1] = neighbors; } } } // Finish neighbor search on this voxel } }); } // Finish adding all visible voxels. // Copy them back to the original voxel array cache.voxels[Vector3.One] = visibleVoxels; }
/// <summary> /// Load chunks for the first time /// </summary> public ChunkManager(GraphicsDevice graphicsDevice, int seed) { // Set the initial seed this.mapSeed = seed; // Get the bounds of the visible world and set up chunk structures int totalChunks = (int)Math.Pow((visibleRadius * 2 + 1), 2); chunkStacks = new Dictionary <Point, ChunkStack>(totalChunks); orderedChunkStacks = new List <ChunkStack>(totalChunks); // Initialize lists for chunk distance ordering chunks = new SpeedyDictionary();// Dictionary<Vector3, Chunk>(totalChunks * chunksPerStack); orderedChunks = new List <Chunk>(totalChunks * chunksPerStack); // Setup biome and cache voxelCache = new VoxelCache(chunkSize, chunkSize, chunkSize); // Populator to add additional objects to chunks //populator = new Engine.TreePopulator(biome, voxelCache, new Random(seed)); }
/// <summary> /// Load voxel data and create a mesh from it /// </summary> public void Load(VoxelCache cache, String filename) { this.voxelData = MagicaVoxImporter.Load(filename); // Setup cache cache.ResetData(); cache.voxels.Add(Vector3.One, new byte[cache.SizeX + 2, cache.SizeZ + 2, cache.SizeY + 2]); // First generate the height map and dense voxel data BuildVoxels(cache); // Second pass: Add visible voxels FindVisible(cache); // Third pass: Build mesh out of visible voxels BuildMesh(cache); // Reset cache cache.voxels.Remove(Vector3.One); cache.ResetData(); }
/// <summary> /// Generate a world chunk /// </summary> public void Setup(VoxelCache cache, Biome biome) { // Create lightweight voxel array. At 2 bits/voxel, Y axis shares 16 voxels per int voxels = new uint[cache.SizeX, cache.SizeZ, cache.SizeY / 16]; cache.voxels.Remove(previousOffset); cache.voxels.Add(offset, new byte[cache.SizeX + 2, cache.SizeZ + 2, cache.SizeY + 2]); // First generate the height map and dense voxel data BuildVoxels(cache, biome); state = ChunkState.VoxelsLoaded; // Remove voxels from cache if chunk doesn't need a mesh if (isEmpty || isCompletelySolid) { cache.voxels.Remove(offset); } // Reset cache cache.ResetData(); }
/// <summary> /// Third step in mesh generation /// Turn visible voxels into meshes /// </summary> private void BuildMesh(VoxelCache cache) { int nextVertex = 0; int nextIndex = 0; Object lockObject = new object(); // Initialize vertex and index data VertexPositionColorNormal[] chunkVertices = new VertexPositionColorNormal[vertexMaxSize]; ushort[] chunkIndices = new ushort[indexMaxSize]; // Create the mesh cubes Parallel.For(1, sizeX + 1, x => { for (int z = 1; z <= sizeZ; ++z) { for (int y = 1; y <= sizeY; ++y) { byte voxel = (byte)cache.voxels[Vector3.One][x, z, y]; if (nextIndex + 72 > indexMaxSize) { // Skip } else if (voxel > 0) { int neighbors = cache.visibleNeighbors[x - 1, z - 1, y - 1]; // Get the corner and edge neighbors // Top corners (bits 7-10) neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x - 1, z - 1, y + 1] > 0) << NeighborBlock.Top_NW; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x + 1, z - 1, y + 1] > 0) << NeighborBlock.Top_NE; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x - 1, z + 1, y + 1] > 0) << NeighborBlock.Top_SW; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x + 1, z + 1, y + 1] > 0) << NeighborBlock.Top_SE; // Top edges (bits 11-14) neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x - 1, z, y + 1] > 0) << NeighborBlock.Top_W; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x + 1, z, y + 1] > 0) << NeighborBlock.Top_E; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x, z - 1, y + 1] > 0) << NeighborBlock.Top_N; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x, z + 1, y + 1] > 0) << NeighborBlock.Top_S; // Bottom corners (bits 15-18) neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x - 1, z - 1, y - 1] > 0) << NeighborBlock.Bottom_NW; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x + 1, z - 1, y - 1] > 0) << NeighborBlock.Bottom_NE; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x - 1, z + 1, y - 1] > 0) << NeighborBlock.Bottom_SW; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x + 1, z + 1, y - 1] > 0) << NeighborBlock.Bottom_SE; // Bottom edges (bits 19-22) neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x - 1, z, y - 1] > 0) << NeighborBlock.Bottom_W; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x + 1, z, y - 1] > 0) << NeighborBlock.Bottom_E; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x, z - 1, y - 1] > 0) << NeighborBlock.Bottom_N; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x, z + 1, y - 1] > 0) << NeighborBlock.Bottom_S; // Middle side corners (bits 23-26) neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x - 1, z - 1, y] > 0) << NeighborBlock.Middle_NW; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x + 1, z - 1, y] > 0) << NeighborBlock.Middle_NE; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x - 1, z + 1, y] > 0) << NeighborBlock.Middle_SW; neighbors |= Convert.ToInt32(cache.voxels[Vector3.One][x + 1, z + 1, y] > 0) << NeighborBlock.Middle_SE; // Set the cube color and shade uint color = colors[voxel]; float[] shades = { 1f, 1f, 1f, 1f, 1f, 1f }; // Add the cube Cube cube = new Cube(new Vector3(x, y, z), color, shades, neighbors); lock (lockObject) { // Point next index value to number of vertices int indexOffset = nextVertex; for (int v = 0; v < cube.Vertices.Length; v++) { chunkVertices[nextVertex++] = cube.Vertices[v]; } for (int i = 0; i < cube.Indices.Length; i++) { chunkIndices[nextIndex++] = (ushort)(indexOffset + cube.Indices[i]); } } } // Finish adding cube for this voxel } } }); // Create a mesh mesh = new MeshData(); // Create bounding box mesh.bBox = new BoundingBox(extents[0], extents[1]); if (nextIndex > 0) { // Create vertex and index buffers mesh.vb = new VertexBuffer( graphicsDevice, VertexPositionColorNormal.VertexDeclaration, nextVertex, BufferUsage.None ); mesh.ib = new IndexBuffer( graphicsDevice, IndexElementSize.SixteenBits, (ushort)nextIndex, BufferUsage.None ); // Set up vertex and index buffer mesh.vb.SetData(chunkVertices, 0, nextVertex); mesh.ib.SetData(chunkIndices, 0, nextIndex); } // Finished building meshes }
/// <summary> /// Calculate shading for all sides of a cube /// </summary> private float[] ComputeShade(VoxelCache cache, Biome biome, int neighbors, int x, int y, int z) { // Get shade contributions using a ray cast float altShade = (float)(cache.lightVoxels[x, z, y] / 20f); altShade = 1 - (float)Math.Pow(altShade, 1.25f); // Get shade contributions using a ray cast float[] shade = new float[6]; // Shade all the cube sides individually for (int s = 0; s < shade.Length; s++) { // Start at full light shade[s] = 1f; float step = 3f / points[s].Length; // Skipped sides that have neighbors if ((neighbors & 1 << s) == 1) { continue; } for (int j = 0; j < points[s].Length; j++) { if (shade[s] <= 0.1f) { break; } bool found = false; for (float i = 2.5f; i <= 10.5f; i += 1f) { if (found) { continue; } Vector3 newBlock; int chunkIndex = 4; newBlock.X = i * points[s][j].X + x; newBlock.Y = i * points[s][j].Y + y; newBlock.Z = i * points[s][j].Z + z; int nx = (int)Math.Round(newBlock.X) - 1; int ny = (int)Math.Round(newBlock.Y); int nz = (int)Math.Round(newBlock.Z) - 1; // Set the new voxel offset if ray enters another chunk if (nx < 0) { chunkIndex--; } if (nz < 0) { chunkIndex -= 3; } if (nx >= cache.SizeX) { chunkIndex++; } if (nz >= cache.SizeZ) { chunkIndex += 3; } if (nx < 0) { nx += cache.SizeX; } if (nz < 0) { nz += cache.SizeZ; } if (nx >= cache.SizeX) { nx -= cache.SizeX; } if (nz >= cache.SizeZ) { nz -= cache.SizeZ; } // Continue if Y goes out of bounds if (ny < 0 || ny >= cache.SizeY) { continue; } byte voxel = GetVoxel(nx, ny, nz); if (chunkIndex != 4) { if (chunkIndex > 4) { chunkIndex--; } voxel = sideNeighbors[chunkIndex].GetVoxel(nx, ny, nz); } float falloff = 1f / i; if (voxel != (byte)BlockType.Empty) { shade[s] -= (step * falloff); found = true; } } } shade[s] = (altShade > shade[s]) ? shade[s] : altShade; shade[s] = (shade[s] < 0.15f) ? 0.15f : shade[s]; } return(shade); }
/// <summary> /// Second step in chunk generation /// Add visible voxels to the list. /// </summary> private void FindVisible(VoxelCache cache, Biome biome) { // Assign temporary arrays for visible voxels byte[, ,] visibleVoxels = new byte[cache.SizeX + 2, cache.SizeX + 2, cache.SizeY + 2]; byte[, ,] cacheVoxels = cache.voxels[offset]; Parallel.For(1, cache.SizeX + 1, x => { for (int z = 0; z <= cache.SizeZ + 1; ++z) { // Get last shade for x, z location byte shade = biome.lightMask[x, z]; bool shaded = (shade == 15); for (int y = cache.SizeY + 1; y >= 0; --y) { byte voxel = cacheVoxels[x, z, y]; if (voxel != 0) { int neighbors = 0; if (x == 0 || y == 0 || z == 0 || x == cache.SizeX + 1 || y == cache.SizeY + 1 || z == cache.SizeZ + 1) { visibleVoxels[x, z, y] = voxel; } else { // Immediate side neighbors neighbors |= Convert.ToInt32(cacheVoxels[x - 1, z, y] > 0) << NeighborBlock.Middle_W; neighbors |= Convert.ToInt32(cacheVoxels[x + 1, z, y] > 0) << NeighborBlock.Middle_E; neighbors |= Convert.ToInt32(cacheVoxels[x, z - 1, y] > 0) << NeighborBlock.Middle_N; neighbors |= Convert.ToInt32(cacheVoxels[x, z + 1, y] > 0) << NeighborBlock.Middle_S; // Check top and bottom neighbors |= Convert.ToInt32(cacheVoxels[x, z, y - 1] > 0) << NeighborBlock.Bottom_Center; neighbors |= Convert.ToInt32(cacheVoxels[x, z, y + 1] > 0) << NeighborBlock.Top_Center; // Add to the visible list if not occluded on all sides if (neighbors != 0x3f) { visibleVoxels[x, z, y] = voxel; cache.visibleNeighbors[x - 1, z - 1, y - 1] = neighbors; } cache.lightVoxels[x, z, y] = shade; shade = 8; } } // Finish checking this voxel if (y == 1) { biome.lightMask[x, z] = shade; } } } }); // Final voxel list is truncated cache.voxels[offset] = visibleVoxels; }
/// <summary> /// First step in voxel chunk generation /// This could be used to get voxel data only (when mesh is not needed) /// </summary> private void BuildVoxels(VoxelCache cache, Biome biome) { // Generate the dense voxel data Parallel.For(0, cache.SizeX + 2, x => { for (int z = 0; z < cache.SizeX + 2; ++z) { //float slope = heightMap.CalculateSlope(x, z); cache.heightValues[x, z] = biome.SourceHeight[x, z]; int yHeight = (int)cache.heightValues[x, z]; // Add some rivers bool riverbed = false; // Create an alternate height for river banks. // Higher altitudes will have narrower streams float maxRiverHeight = 120f; // Create noise for the river depths float noise = Noise.Simplex.Generate( (float)(((offset.X + x) / (float)(noiseRes * 24f)) + 200f), (float)(((offset.Z + z) / (float)(noiseRes * 24f)) + 200f)); noise += Noise.Simplex.Generate( (float)(((offset.X + x) / (noiseRes * 4f)) + 200f), (float)(((offset.Z + z) / (noiseRes * 4f)) + 200f)) / 5f; noise += Noise.Simplex.Generate( (float)(((offset.X + x) / (noiseRes / 2f)) + 200f), (float)(((offset.Z + z) / (noiseRes / 2f)) + 200f)) / 50f; noise *= MathHelper.Lerp(0, 1.5f, yHeight / maxRiverHeight); noise = (float)Math.Pow(Math.Abs(noise), 0.7f); // Increase humidity around river areas float riverDepth = MathHelper.Lerp(0.8f, 0.98f, (yHeight > maxRiverHeight) ? 1 : yHeight / maxRiverHeight); float surfaceHeight = cache.heightValues[x, z]; // Set depth of river beds float maxRiverBankNoise = 0.10f; if (noise >= 0.05f && noise < maxRiverBankNoise && yHeight < maxRiverHeight) { surfaceHeight -= (0.03f - (noise - 0.05f)) * 50f; surfaceHeight--; } // Set the water level float riverWaterLevel = surfaceHeight - (maxRiverBankNoise - 0.05f) * 100f; riverWaterLevel++; if (noise < 0.05f && yHeight < maxRiverHeight) { riverbed = true; surfaceHeight = (int)(yHeight * riverDepth); surfaceHeight--; } // Add plateau height, first by setting a maximum height float noise2 = Noise.Simplex.Generate( (float)(((offset.X + x) / (float)(noiseRes * 5f)) + 200f), (float)(((offset.Z + z) / (float)(noiseRes * 3.2f)) + 200f)); noise2 += Noise.Simplex.Generate( (float)(((offset.X + x) / (float)(noiseRes * 1.6f)) + 200f), (float)(((offset.Z + z) / (float)(noiseRes * 2)) + 200f)) / 5f; float plateauRange = 50f; int plateauHeight = (int)((noise2 / 2f + 0.5f) * plateauRange); plateauHeight += 70; for (int y = 0; y < cache.SizeY + 2; ++y) { // Get voxels from height indexes BlockType block = BlockType.Empty; // Add block type depending on relation to surface if (y + offset.Y < (int)surfaceHeight) { block = BlockType.Surface; } if (riverbed && y + offset.Y < riverWaterLevel) { block = BlockType.Water; } if (y + offset.Y == (int)surfaceHeight && y + offset.Y < yHeight) { block = BlockType.Surface; } // Add actual plateau blocks here if (y + offset.Y >= yHeight && y + offset.Y <= plateauHeight) { noise2 = Noise.Simplex.Generate( (float)(((offset.X + x) / (noiseRes * 1.5f)) + 200f), (float)(((offset.Y + y) / (noiseRes * 3f)) + 200f), (float)(((offset.Z + z) / (noiseRes * 1.25f)) + 200f)); noise2 += Noise.Simplex.Generate( (float)(((offset.X + x) / (noiseRes / 1.5f)) + 200f), (float)(((offset.Y + y) / (noiseRes)) + 200f), (float)(((offset.Z + z) / (noiseRes / 1.25f)) + 200f)) / 5f; noise2 += Noise.Simplex.Generate( (float)(((offset.X + x) / (noiseRes / 5f)) + 200f), (float)(((offset.Y + y) / (noiseRes / 2f)) + 200f), (float)(((offset.Z + z) / (noiseRes / 5f)) + 200f)) / 10f; // Adjust plateau height around rivers float threshold = 0.3f; if (noise < maxRiverBankNoise * 3f) { noise2 *= (float)Math.Pow((noise / (maxRiverBankNoise * 3f)), 0.5); } if (noise2 > threshold) { // Inner blocks block = BlockType.Dirt; if (y + offset.Y == plateauHeight) { block = BlockType.Surface; } } } // Set upper and lower bounds if (block != BlockType.Empty) { cache.voxels[offset][x, z, y] = (byte)block; isEmpty = false; } else if (isCompletelySolid) { isCompletelySolid = false; } } // Finished updating this voxel } }); // Add extra objects to the surface such as trees for (int i = 0; i < 9; i++) { int x = i / 3 - 1; int z = i % 3 - 1; GenerateExtraObjects(biome, cache, x, z); } // Set lightweight voxel data for collisions/physics Parallel.For(1, cache.SizeX + 1, x => { for (int z = 1; z < cache.SizeZ + 1; ++z) { for (int y = 0; y < cache.SizeY; ++y) { int voxelY = y / 16; int voxelBits = (y % 16) * 2; uint voxel = voxels[x - 1, z - 1, voxelY]; // Add 1 to voxel data BlockType block = (BlockType)cache.voxels[offset][x, z, y]; if (block != BlockType.Empty) { voxel = voxel | ((uint)1 << voxelBits); } voxels[x - 1, z - 1, voxelY] = voxel; } } }); // Set bounding box extents extents[0] = new Vector3(offset.X, offset.Y, offset.Z); extents[1] = new Vector3(offset.X + cache.SizeX, offset.Y + cache.SizeY, offset.Z + cache.SizeX); // Create temp mesh and bounding box mesh = new MeshData(); mesh.bBox = new BoundingBox(extents[0], extents[1]); }