Compare commits
4 Commits
main
...
c81cd3c561
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c81cd3c561 | ||
|
|
50f2bc4221 | ||
|
|
af7a83300f | ||
|
|
6eb17b6c5d |
Binary file not shown.
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 9.3 KiB |
@@ -44,7 +44,7 @@ public sealed class Game : GameWindow
|
||||
this._world.SetChunk(new Vector3i(x, 0, z), this.BuildChunk());
|
||||
}
|
||||
|
||||
ChunkMesher mesher = new();
|
||||
ChunkMesher mesher = new(this._texture.Width, this._texture.Height);
|
||||
foreach ((Vector3i pos, Chunk chunk) in this._world)
|
||||
{
|
||||
Vector3i origin = pos * Chunk.Size;
|
||||
|
||||
@@ -7,6 +7,9 @@ public sealed class Texture : IDisposable
|
||||
{
|
||||
private readonly int _handle;
|
||||
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
|
||||
public Texture(string path)
|
||||
{
|
||||
this._handle = GL.GenTexture();
|
||||
@@ -15,6 +18,8 @@ public sealed class Texture : IDisposable
|
||||
StbImage.stbi_set_flip_vertically_on_load(1);
|
||||
using FileStream stream = File.OpenRead(path);
|
||||
ImageResult image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha);
|
||||
this.Width = image.Width;
|
||||
this.Height = image.Height;
|
||||
|
||||
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
|
||||
image.Width, image.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, image.Data);
|
||||
|
||||
@@ -49,4 +49,26 @@ public class BlockRegistryTests
|
||||
Assert.Equal((ushort)2, registry.Dirt.Id);
|
||||
Assert.Equal((ushort)3, registry.Grass.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SandAndSnow_AreOpaque_WaterIsOpaque()
|
||||
{
|
||||
var registry = new BlockRegistry();
|
||||
Assert.True(registry.Get(4).IsOpaque); // Sand
|
||||
Assert.True(registry.Get(5).IsOpaque); // Snow
|
||||
Assert.True(registry.Get(6).IsOpaque); // Water (single-pass renderer => opaque)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_ResolvesAllSevenBlocks()
|
||||
{
|
||||
var registry = new BlockRegistry();
|
||||
Assert.Equal("Air", registry.Get(0).Name);
|
||||
Assert.Equal("Stone", registry.Get(1).Name);
|
||||
Assert.Equal("Dirt", registry.Get(2).Name);
|
||||
Assert.Equal("Grass", registry.Get(3).Name);
|
||||
Assert.Equal("Sand", registry.Get(4).Name);
|
||||
Assert.Equal("Snow Block", registry.Get(5).Name);
|
||||
Assert.Equal("Water", registry.Get(6).Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class ChunkMesherTests
|
||||
[Fact]
|
||||
public void EmptyChunk_ProducesNoFaces()
|
||||
{
|
||||
var mesh = new ChunkMesher().Build(new Chunk(), new BlockRegistry());
|
||||
var mesh = new ChunkMesher(128, 128).Build(new Chunk(), new BlockRegistry());
|
||||
Assert.Empty(mesh);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class ChunkMesherTests
|
||||
var chunk = new Chunk();
|
||||
chunk[8, 8, 8] = 1; // stone
|
||||
|
||||
var mesh = new ChunkMesher().Build(chunk, new BlockRegistry());
|
||||
var mesh = new ChunkMesher(128, 128).Build(chunk, new BlockRegistry());
|
||||
|
||||
Assert.Equal(6, FaceCount(mesh));
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class ChunkMesherTests
|
||||
for (int z = 0; z < Chunk.Size; z++)
|
||||
chunk[x, y, z] = 1; // stone
|
||||
|
||||
var mesh = new ChunkMesher().Build(chunk, new BlockRegistry());
|
||||
var mesh = new ChunkMesher(128, 128).Build(chunk, new BlockRegistry());
|
||||
|
||||
// 6 sides * 16 * 16 visible faces
|
||||
Assert.Equal(6 * Chunk.Size * Chunk.Size, FaceCount(mesh));
|
||||
@@ -52,10 +52,32 @@ public class ChunkMesherTests
|
||||
var chunk = new Chunk();
|
||||
chunk[0, 0, 0] = 1; // corner block: all 6 neighbors are air/out-of-bounds -> 6 faces
|
||||
|
||||
var mesh = new ChunkMesher().Build(chunk, new BlockRegistry());
|
||||
var mesh = new ChunkMesher(128, 128).Build(chunk, new BlockRegistry());
|
||||
|
||||
// Pins the layout: 6 faces * 6 vertices/face * 6 floats/vertex.
|
||||
Assert.Equal(6 * VerticesPerFace * FloatsPerVertex, mesh.Length);
|
||||
Assert.Equal(0, mesh.Length % FloatsPerVertex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mesher_UsesAtlasDimensions_ForTileSize()
|
||||
{
|
||||
var chunk = new Chunk();
|
||||
chunk[0, 0, 0] = 1; // stone, tile (1,0)
|
||||
|
||||
// 128px atlas, 32px tiles => 4x4 grid, normalized tile size 0.25.
|
||||
var mesh = new ChunkMesher(128, 128).Build(chunk, new BlockRegistry());
|
||||
|
||||
// Gather all U coords (offset 3) and V coords (offset 4); each must be a
|
||||
// multiple of 0.25 within [0,1].
|
||||
for (int i = 0; i < mesh.Length; i += 6)
|
||||
{
|
||||
float u = mesh[i + 3];
|
||||
float v = mesh[i + 4];
|
||||
Assert.InRange(u, 0f, 1f);
|
||||
Assert.InRange(v, 0f, 1f);
|
||||
Assert.True(System.Math.Abs(u * 4f - System.MathF.Round(u * 4f)) < 1e-4f, $"u={u} not on 0.25 grid");
|
||||
Assert.True(System.Math.Abs(v * 4f - System.MathF.Round(v * 4f)) < 1e-4f, $"v={v} not on 0.25 grid");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
World/Blocks/BlockIds.txt
Normal file
7
World/Blocks/BlockIds.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Air = 0
|
||||
Stone = 1
|
||||
Dirt = 2
|
||||
Grass = 3
|
||||
Sand = 4
|
||||
Snow = 5
|
||||
Water = 6
|
||||
@@ -8,11 +8,15 @@ public sealed class BlockRegistry
|
||||
public StoneBlock Stone { get; } = new();
|
||||
public DirtBlock Dirt { get; } = new();
|
||||
public GrassBlock Grass { get; } = new();
|
||||
public SandBlock Sand { get; } = new();
|
||||
public SnowBlock Snow { get; } = new();
|
||||
public WaterBlock Water { get; } = new();
|
||||
|
||||
public BlockRegistry()
|
||||
{
|
||||
// Index by id. Order does not matter; slot = block.Id.
|
||||
IBlock[] blocks = new IBlock[] { this.Air, this.Stone, this.Dirt, this.Grass };
|
||||
IBlock[] blocks = new IBlock[]
|
||||
{ this.Air, this.Stone, this.Dirt, this.Grass, this.Sand, this.Snow, this.Water };
|
||||
this._byId = new IBlock[blocks.Length];
|
||||
foreach (IBlock block in blocks)
|
||||
{
|
||||
|
||||
@@ -9,8 +9,8 @@ public sealed class GrassBlock : IBlock
|
||||
|
||||
public TileCoord GetTextureTile(BlockFace face) => face switch
|
||||
{
|
||||
BlockFace.Top => new(0, 1), // grass top
|
||||
BlockFace.Bottom => new(0, 0), // dirt
|
||||
_ => new(1, 1) // grass side
|
||||
BlockFace.Top => new TileCoord(0, 1), // grass top
|
||||
BlockFace.Bottom => new TileCoord(0, 0), // dirt
|
||||
_ => new TileCoord(1, 1) // grass side
|
||||
};
|
||||
}
|
||||
|
||||
10
World/Blocks/SandBlock.cs
Normal file
10
World/Blocks/SandBlock.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace VoxelEngine.World.Blocks;
|
||||
|
||||
public class SandBlock : IBlock
|
||||
{
|
||||
public ushort Id => 4;
|
||||
public string Name => "Sand";
|
||||
public bool IsOpaque => true;
|
||||
public bool IsSolid => true;
|
||||
public TileCoord GetTextureTile(BlockFace face) => new(2, 1);
|
||||
}
|
||||
16
World/Blocks/SnowBlock.cs
Normal file
16
World/Blocks/SnowBlock.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace VoxelEngine.World.Blocks;
|
||||
|
||||
public class SnowBlock : IBlock
|
||||
{
|
||||
public ushort Id => 5;
|
||||
public string Name => "Snow Block";
|
||||
public bool IsOpaque => true;
|
||||
public bool IsSolid => true;
|
||||
|
||||
public TileCoord GetTextureTile(BlockFace face) => face switch
|
||||
{
|
||||
BlockFace.Top => new TileCoord(2, 0),
|
||||
BlockFace.Bottom => new TileCoord(0, 0),
|
||||
_ => new TileCoord(3, 0)
|
||||
};
|
||||
}
|
||||
5
World/Blocks/TileCoord.cs
Normal file
5
World/Blocks/TileCoord.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace VoxelEngine.World.Blocks;
|
||||
|
||||
// Position of a tile within the texture atlas, in tile units.
|
||||
// Origin (0,0) is the top-left tile; X increases right, Y increases down.
|
||||
public readonly record struct TileCoord(int X, int Y);
|
||||
10
World/Blocks/WaterBlock.cs
Normal file
10
World/Blocks/WaterBlock.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace VoxelEngine.World.Blocks;
|
||||
|
||||
public class WaterBlock : IBlock
|
||||
{
|
||||
public ushort Id => 6;
|
||||
public string Name => "Water";
|
||||
public bool IsOpaque => true;
|
||||
public bool IsSolid => false;
|
||||
public TileCoord GetTextureTile(BlockFace face) => new(3, 3);
|
||||
}
|
||||
@@ -5,9 +5,15 @@ namespace VoxelEngine.World.Chunk;
|
||||
public sealed class ChunkMesher : IChunkMesher
|
||||
{
|
||||
private const int TileSizePixels = 32; // each tile is 32x32 pixels
|
||||
private const int AtlasSizePixels = 64; // atlas is 64x64 pixels (2x2 grid)
|
||||
|
||||
private const float TileSize = (float)TileSizePixels / AtlasSizePixels; // normalized tile size
|
||||
private readonly float _tileU; // normalized tile width
|
||||
private readonly float _tileV; // normalized tile height
|
||||
|
||||
public ChunkMesher(int atlasWidthPixels, int atlasHeightPixels)
|
||||
{
|
||||
this._tileU = (float)ChunkMesher.TileSizePixels / atlasWidthPixels;
|
||||
this._tileV = (float)ChunkMesher.TileSizePixels / atlasHeightPixels;
|
||||
}
|
||||
|
||||
// Per-face geometry: base corner + two in-plane edge vectors (u, v) with
|
||||
// cross(u, v) == outward normal, guaranteeing CCW winding from outside.
|
||||
@@ -72,18 +78,18 @@ public sealed class ChunkMesher : IChunkMesher
|
||||
: (neighborOpaqueOutside?.Invoke(nx, ny, nz) ?? false);
|
||||
if (neighborOpaque) continue;
|
||||
|
||||
ChunkMesher.AddFace(verts, x, y, z, face, block.GetTextureTile(face.Kind));
|
||||
this.AddFace(verts, x, y, z, face, block.GetTextureTile(face.Kind));
|
||||
}
|
||||
}
|
||||
|
||||
return verts.ToArray();
|
||||
}
|
||||
|
||||
private static void AddFace(List<float> verts, int x, int y, int z, Face face, TileCoord tile)
|
||||
private void AddFace(List<float> verts, int x, int y, int z, Face face, TileCoord tile)
|
||||
{
|
||||
float uBase = tile.X * ChunkMesher.TileSize;
|
||||
float uBase = tile.X * this._tileU;
|
||||
// Image is flipped on load, so offset from the top to keep tile (0,0) = top-left.
|
||||
float vBase = 1f - (tile.Y + 1) * ChunkMesher.TileSize;
|
||||
float vBase = 1f - (tile.Y + 1) * this._tileV;
|
||||
|
||||
foreach (int corner in ChunkMesher.Triangle)
|
||||
{
|
||||
@@ -93,8 +99,8 @@ public sealed class ChunkMesher : IChunkMesher
|
||||
float py = y + face.Base.y + face.U.y * cu + face.V.y * cv;
|
||||
float pz = z + face.Base.z + face.U.z * cu + face.V.z * cv;
|
||||
|
||||
float u = uBase + cu * ChunkMesher.TileSize;
|
||||
float v = vBase + cv * ChunkMesher.TileSize;
|
||||
float u = uBase + cu * this._tileU;
|
||||
float v = vBase + cv * this._tileV;
|
||||
|
||||
verts.Add(px); verts.Add(py); verts.Add(pz);
|
||||
verts.Add(u); verts.Add(v);
|
||||
|
||||
Reference in New Issue
Block a user