Compare commits

...

22 Commits

Author SHA1 Message Date
maxwes08
66f26bc2e3 removed build error 2025-12-12 10:31:35 +01:00
maxwes08
2e72dd564e started on sprite support 2025-12-09 09:49:10 +01:00
maxwes08
35bc49c0f8 cleaned up 2025-11-04 09:12:24 +01:00
maxwes08
cad22d3c64 removed sprinting 2025-11-04 09:08:28 +01:00
maxwes08
7835ade2c1 fixed movement 2025-10-07 10:37:46 +02:00
bd2c87ddd1 player and entity physics improvements 2025-10-01 00:40:47 +02:00
maxwes08
11f76ca429 player added 2025-09-30 10:58:12 +02:00
maxwes08
38dccf0a84 better world gen 2025-09-29 10:55:37 +02:00
maxwes08
9a61dfd74c chunbk face culling 2025-09-22 10:56:55 +02:00
81ef6d8a29 fix render offset 2025-09-21 22:55:22 +02:00
maxwes08
e1bb0b3683 raycasting changes 2025-09-16 10:06:01 +02:00
maxwes08
40dd5c3a9e peak 2025-09-15 10:48:45 +02:00
50f9d4c0c8 fixed rendering 2025-09-09 19:43:27 +02:00
maxwes08
ce456b6b26 edited rendering 2025-09-09 13:31:44 +02:00
maxwes08
b6655d71d9 update 2025-09-09 10:57:28 +02:00
maxwes08
3678eaa5f8 chunk meshes 2025-09-03 15:08:21 +02:00
94ebc4ace4 added block breaking 2025-09-03 00:37:42 +02:00
00713db79e Culling, rendering improvements and optimizations, block removing test. 2025-09-02 22:43:35 +02:00
a9cab195b6 added texture atlas 2025-09-02 17:55:25 +02:00
maxwes08
6fb19c415f cleaned up all the code and updated 2025-09-02 13:36:14 +02:00
Max Westerlund
71c5f3a3aa update 2025-09-02 13:17:15 +02:00
b6f9966eb9 edited shape 2025-08-26 16:50:01 +02:00
25 changed files with 4458 additions and 90 deletions

41
AABB.cs Normal file
View File

@@ -0,0 +1,41 @@
using OpenTK.Mathematics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Voxel
{
public struct AABB
{
public Vector3 Min;
public Vector3 Max;
public AABB(Vector3 min, Vector3 max)
{
Min = min;
Max = max;
}
public static AABB FromCenter(Vector3 center, float width, float height, float depth)
{
Vector3 half = new Vector3(width / 2f, 0, depth / 2f);
return new AABB(center - half, center + new Vector3(half.X, height, half.Z));
}
public bool Intersects(AABB other)
{
return (Min.X <= other.Max.X && Max.X >= other.Min.X) &&
(Min.Y <= other.Max.Y && Max.Y >= other.Min.Y) &&
(Min.Z <= other.Max.Z && Max.Z >= other.Min.Z);
}
public bool Contains(Vector3 point)
{
return (point.X >= Min.X && point.X <= Max.X) &&
(point.Y >= Min.Y && point.Y <= Max.Y) &&
(point.Z >= Min.Z && point.Z <= Max.Z);
}
}
}

12
BlockData.cs Normal file
View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Voxel
{
public class BlockData
{
}
}

94
Blocks.cs Normal file
View File

@@ -0,0 +1,94 @@
namespace Voxel
{
public enum Blocks : byte
{
Air,
Stone,
Dirt,
OakPlanks,
Grass,
Bedrock,
Sand
}
public enum Orientation : byte
{
West = 0, // + X
East = 1, // - X
Top = 2, // + Y
Bottom = 3,// - Y
North = 4, // + Z
South = 5, // - Z
}
public class BlockDefinition
{
public Blocks BlockType;
public Textures[] FaceTextures;
public BlockDefinition(Blocks type, Textures singleTexture)
{
BlockType = type;
FaceTextures = new Voxel.Textures[6];
for (int i = 0; i < 6; i++) FaceTextures[i] = singleTexture;
}
public BlockDefinition(
Blocks type,
Textures west,
Textures east,
Textures top,
Textures bottom,
Textures north,
Textures south
)
{
BlockType = type;
FaceTextures = new Textures[]
{
west, east, top, bottom, north, south
};
}
}
public static class BlockDefinitions
{
public static readonly Dictionary<Blocks, BlockDefinition> Blocks;
static BlockDefinitions()
{
Blocks = new Dictionary<Blocks, BlockDefinition>
{
{Voxel.Blocks.Stone, new BlockDefinition(
Voxel.Blocks.Stone, Textures.Stone
)},
{Voxel.Blocks.Dirt, new BlockDefinition(
Voxel.Blocks.Dirt, Textures.Dirt
)},
{Voxel.Blocks.OakPlanks, new BlockDefinition(
Voxel.Blocks.OakPlanks, Textures.OakPlanks
)},
{Voxel.Blocks.Bedrock, new BlockDefinition(
Voxel.Blocks.Bedrock, Textures.Bedrock
)},
{Voxel.Blocks.Sand, new BlockDefinition(
Voxel.Blocks.Sand, Textures.Sand
)},
{ Voxel.Blocks.Grass, new BlockDefinition(
Voxel.Blocks.Grass,
Voxel.Textures.GrassSide, // West
Voxel.Textures.GrassSide, // East
Voxel.Textures.GrassTop, // Top
Voxel.Textures.Dirt, // Bottom
Voxel.Textures.GrassSide, // North
Voxel.Textures.GrassSide // South
)},
};
}
}
}

71
Camera.cs Normal file
View File

@@ -0,0 +1,71 @@
using OpenTK.Mathematics;
namespace Voxel
{
static class Camera
{
public static Vector3 Position = new Vector3(-8, 16, -8);
public static float Pitch = -22.5f;
public static float Yaw = 0f;
public static float FOV = 60f;
public static float TargetFOV = FOV;
public static float FOVLerpSpeed = 10f;
public static float Speed = 5f;
public static float ShiftSpeed = 20f;
private static int _width;
private static int _height;
public static Matrix4 view =>
Matrix4.LookAt(Position, Position + Front, Vector3.UnitY);
public static Matrix4 projection;
public static Vector3 Front
{
get
{
float yawOffset = Yaw - 90f;
Vector3 front;
front.X = MathF.Cos(MathHelper.DegreesToRadians(yawOffset)) * MathF.Cos(MathHelper.DegreesToRadians(Pitch));
front.Y = MathF.Sin(MathHelper.DegreesToRadians(Pitch));
front.Z = MathF.Sin(MathHelper.DegreesToRadians(yawOffset)) * MathF.Cos(MathHelper.DegreesToRadians(Pitch));
return front.Normalized();
}
}
public static void UpdateMouse(Vector2 delta)
{
float sensitivity = 0.1f;
Yaw += delta.X * sensitivity;
Pitch -= delta.Y * sensitivity;
Pitch = MathHelper.Clamp(Pitch, -89f, 89f);
}
public static void UpdateProjection()
{
float fov = MathHelper.DegreesToRadians(FOV);
float aspectRatio = _width / (float)_height;
float near = 0.1f;
float far = 1000f;
projection = Matrix4.CreatePerspectiveFieldOfView(fov, aspectRatio, near, far);
}
public static void UpdateSize(int width, int height)
{
_width = width;
_height = height;
UpdateProjection();
}
public static void UpdateFOV(float deltaTime, float alpha)
{
float currentFOV = MathHelper.Lerp(FOV, TargetFOV, FOVLerpSpeed * deltaTime);
Camera.FOV = currentFOV;
Camera.UpdateProjection();
}
}
}

187
Chunk.cs Normal file
View File

@@ -0,0 +1,187 @@
namespace Voxel
{
public class Chunk
{
public static int Size = 16;
public static int Height = 256;
public readonly int X;
public readonly int Y;
private ChunkMesh _chunkMesh;
private Dictionary<ushort, BlockData> _blockData;
private Blocks[] _blocks;
public Dictionary<Orientation, Chunk> Neighbors = new();
public Chunk(int x, int y)
{
X = x;
Y = y;
_blockData = new Dictionary<ushort, BlockData>();
_blocks = new Blocks[Size * Size * Height];
_chunkMesh = new ChunkMesh(X, Y);
Initialize();
}
public void SetBlock(int x, int y, int z, Blocks block, bool updateMesh = true)
{
int i = GetBlockIndex(x, y, z);
if (i == -1) return;
_blocks[i] = block;
if (updateMesh)
{
UpdateChunkMesh();
Renderer.MarkBuffersDirty();
}
}
public void SetBlockIndex(int i, Blocks block)
{
_blocks[i] = block;
}
public Blocks GetBlock(int x, int y, int z)
{
int i = GetBlockIndex(x, y, z);
if (i == -1) return Blocks.Air;
return _blocks[i];
}
public BlockData GetBlockData(int x, int y, int z)
{
return new BlockData();
}
private void Initialize()
{
for (int x = 0; x < Size; x++)
{
for (int z = 0; z < Size; z++)
{
var position = GetWorldCoordinates(x, 0, z);
int height = Worldgen.GetHeight(position.x, position.z);
for (int y = 0; y < 256; y++)
{
Blocks block = Worldgen.GetBlock(y, height);
SetBlock(x, y, z, block, false);
}
}
}
UpdateChunkMesh();
}
// todo
public (int x, int y, int z) IndexToPosition(int i)
{
int x = 0;
int y = 0;
int z = 0;
return (x, y, z);
}
public (int x, int y, int z) GetWorldCoordinates(int x, int y, int z)
{
x += (Size * X);
z += (Size * Y);
return (x, y, z);
}
private int GetBlockIndex(int x, int y, int z)
{
if (x < 0 || x > 15 || y < 0 || y > 255 || z < 0 || z > 15)
return -1;
return x + z * Size + y * Size * Size;
}
private static readonly (int dx, int dy, int dz)[] Offsets = new (int, int, int)[6]
{
( 1, 0, 0), // +X
(-1, 0, 0), // -X
( 0, 1, 0), // +Y
( 0, -1, 0), // -Y
( 0, 0, 1), // +Z
( 0, 0, -1) // -Z
};
public void UpdateChunkMesh()
{
List<FaceData> faces = new List<FaceData>(Size * Size * Height / 2);
for (int x = 0; x < Size; x++)
{
for (int z = 0; z < Size; z++)
{
for (int y = 0; y < Height; y++)
{
for (byte face = 0; face < 6; face++)
{
int indexBase = y * Size * Size + z * Size + x;
Blocks block = _blocks[indexBase];
void AddFace()
{
FaceData faceData = new FaceData();
faceData.Facing = (Orientation)face;
faceData.Texture = BlockDefinitions.Blocks[block].FaceTextures[face];
faceData.X = (byte)x;
faceData.Y = (byte)y;
faceData.Z = (byte)z;
faces.Add(faceData);
}
if (block == Blocks.Air) continue; // ignore if air
int nx = x + Offsets[face].dx;
int ny = y + Offsets[face].dy;
int nz = z + Offsets[face].dz;
// check neighbor, ignore if at chunk edge
int ni = GetBlockIndex(nx, ny, nz);
if (GetBlockIndex(nx, ny, nz) == -1)
{
if (Neighbors.TryGetValue((Orientation)face, out Chunk neighbor) && neighbor != null)
{
int localX = nx;
int localZ = nz;
if (nx < 0) localX = nx + Size;
if (nx >= Size) localX = nx - Size;
if (nz < 0) localZ = nz + Size;
if (nz >= Size) localZ = nz - Size;
Blocks neighborBlock = neighbor.GetBlock(localX, y, localZ);
if (neighborBlock != Blocks.Air)
continue;
}
AddFace();
continue;
}
if (_blocks[ni] == Blocks.Air)
{
AddFace();
continue;
}
}
}
}
}
_chunkMesh.SetFaces(faces);
}
public ChunkMesh GetChunkMesh()
{
return _chunkMesh;
}
}
}

44
ChunkMesh.cs Normal file
View File

@@ -0,0 +1,44 @@
using OpenTK.Graphics.OpenGL4;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Voxel
{
public class ChunkMesh
{
public int X;
public int Y;
private byte[] _packedData;
public bool NeedsUpdate = false;
public int Size = 0;
private List<FaceData> _faces;
public ChunkMesh(int x, int y)
{
_packedData = new byte[0];
X = x;
Y = y;
}
public void SetFaces(List<FaceData> faces)
{
Size = faces.Count;
_faces = faces;
NeedsUpdate = true;
}
public byte[] GetPackedData()
{
if (NeedsUpdate)
{
_packedData = _faces.SelectMany(f => f.Pack()).ToArray();
}
return _packedData;
}
}
}

449
Entity.cs Normal file
View File

@@ -0,0 +1,449 @@
using OpenTK.Mathematics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Voxel
{
public class Entity
{
public Vector3 Position;
public Vector3 Velocity;
public bool OnGround;
public float Rotation;
public float Width;
public float Height;
private float _gravity = 0.08f;
private float _terminalVelocity = -3.92f;
private float _airMultiplier = 0.91f;
private float _groundMultiplier = 0.6f;
private const float COLLISION_EPSILON = 0.01f;
protected World _world;
public Entity(Vector3 position, float width, float height, World world)
{
Position = position;
Width = width;
Height = height;
_world = world;
}
public void Tick()
{
Vector3 desiredMovement = Velocity;
Vector3 newPosition = Position;
CheckAndResolveCollisions(ref newPosition, desiredMovement);
Position = newPosition;
if (!OnGround)
{
Vector3 acceleration = new Vector3(0, -_gravity, 0);
Velocity += acceleration;
Velocity *= _airMultiplier;
if (Velocity.Y < _terminalVelocity)
{
Velocity.Y = _terminalVelocity;
}
}
else
{
Velocity = new Vector3(Velocity.X * _groundMultiplier, 0f, Velocity.Z * _groundMultiplier);
}
UpdateOnGround();
}
public void CheckAndResolveCollisions(ref Vector3 position, Vector3 movement)
{
Vector3 originalPosition = position;
// Try full movement first
AABB futureBox = GetBoundingBoxAt(originalPosition + movement);
if (!HasCollision(futureBox))
{
// No collision, apply full movement
position = originalPosition + movement;
return;
}
// Collision detected, resolve each axis separately but independently
position = originalPosition;
// Resolve Y collision (always first for ground detection)
ResolveYCollisionIndependent(ref position, movement.Y);
// Resolve X and Z collisions independently of each other
Vector3 tempPosX = new Vector3(position.X + movement.X, position.Y, position.Z);
if (!HasCollision(GetBoundingBoxAt(tempPosX)))
{
position.X = tempPosX.X;
}
else
{
ResolveXCollisionIndependent(ref position, movement.X);
}
Vector3 tempPosZ = new Vector3(position.X, position.Y, position.Z + movement.Z);
if (!HasCollision(GetBoundingBoxAt(tempPosZ)))
{
position.Z = tempPosZ.Z;
}
else
{
ResolveZCollisionIndependent(ref position, movement.Z);
}
}
private void ResolveYCollisionIndependent(ref Vector3 position, float velocityY)
{
if (velocityY == 0) return;
Vector3 testPos = new Vector3(position.X, position.Y + velocityY, position.Z);
AABB testBox = GetBoundingBoxAt(testPos);
if (HasCollision(testBox))
{
if (velocityY > 0) // Hitting ceiling
{
float ceilingY = GetCeilingHeight(testBox);
position.Y = ceilingY - (Height / 2) - COLLISION_EPSILON;
Velocity.Y = 0;
}
else // Hitting floor
{
float floorY = GetFloorHeight(testBox);
position.Y = floorY + (Height / 2) + COLLISION_EPSILON;
Velocity.Y = 0;
OnGround = true;
}
}
else
{
position.Y += velocityY;
}
}
private void ResolveXCollisionIndependent(ref Vector3 position, float velocityX)
{
if (velocityX == 0) return;
// Check if we're already inside a block at current position
AABB currentBox = GetBoundingBoxAt(new Vector3(position.X, position.Y, position.Z));
if (HasCollision(currentBox))
{
// Already inside a block, allow movement to escape
position.X += velocityX;
return;
}
float direction = Math.Sign(velocityX);
float checkDistance = Math.Abs(velocityX) + COLLISION_EPSILON;
for (float offset = 0; offset <= checkDistance; offset += 0.01f)
{
float testX = position.X + (offset * direction);
AABB testBox = GetBoundingBoxAt(new Vector3(testX, position.Y, position.Z));
if (HasCollision(testBox))
{
if (direction > 0) // Moving right
{
position.X = GetRightWallPosition(testBox) - (Width / 2) - COLLISION_EPSILON;
}
else // Moving left
{
position.X = GetLeftWallPosition(testBox) + (Width / 2) + COLLISION_EPSILON;
}
Velocity.X = 0;
return;
}
}
// No collision found, apply full movement
position.X += velocityX;
}
private void ResolveZCollisionIndependent(ref Vector3 position, float velocityZ)
{
if (velocityZ == 0) return;
// Check if we're already inside a block at current position
AABB currentBox = GetBoundingBoxAt(new Vector3(position.X, position.Y, position.Z));
if (HasCollision(currentBox))
{
// Already inside a block, allow movement to escape
position.Z += velocityZ;
return;
}
float direction = Math.Sign(velocityZ);
float checkDistance = Math.Abs(velocityZ) + COLLISION_EPSILON;
for (float offset = 0; offset <= checkDistance; offset += 0.01f)
{
float testZ = position.Z + (offset * direction);
AABB testBox = GetBoundingBoxAt(new Vector3(position.X, position.Y, testZ));
if (HasCollision(testBox))
{
if (direction > 0) // Moving forward
{
position.Z = GetFrontWallPosition(testBox) - (Width / 2) - COLLISION_EPSILON;
}
else // Moving backward
{
position.Z = GetBackWallPosition(testBox) + (Width / 2) + COLLISION_EPSILON;
}
Velocity.Z = 0;
return;
}
}
// No collision found, apply full movement
position.Z += velocityZ;
}
private float GetFloorHeight(AABB box)
{
int minX = (int)MathF.Floor(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X);
int minZ = (int)MathF.Floor(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z);
int checkY = (int)MathF.Floor(box.Min.Y);
float highestFloor = float.MinValue;
for (int x = minX; x <= maxX; x++)
{
for (int z = minZ; z <= maxZ; z++)
{
Blocks block = _world.GetBlock(x, checkY, z);
if (block != Blocks.Air)
{
highestFloor = Math.Max(highestFloor, checkY + 1); // Top of the block
}
}
}
return highestFloor != float.MinValue ? highestFloor : box.Min.Y;
}
private float GetCeilingHeight(AABB box)
{
int minX = (int)MathF.Floor(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X);
int minZ = (int)MathF.Floor(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z);
int checkY = (int)MathF.Floor(box.Max.Y);
float lowestCeiling = float.MaxValue;
for (int x = minX; x <= maxX; x++)
{
for (int z = minZ; z <= maxZ; z++)
{
Blocks block = _world.GetBlock(x, checkY, z);
if (block != Blocks.Air)
{
lowestCeiling = Math.Min(lowestCeiling, checkY); // Bottom of the block
}
}
}
return lowestCeiling != float.MaxValue ? lowestCeiling : box.Max.Y;
}
private float GetLeftWallPosition(AABB box)
{
int minY = (int)MathF.Floor(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y);
int minZ = (int)MathF.Floor(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z);
int checkX = (int)MathF.Floor(box.Min.X);
float rightmostWall = float.MinValue;
for (int y = minY; y <= maxY; y++)
{
for (int z = minZ; z <= maxZ; z++)
{
Blocks block = _world.GetBlock(checkX, y, z);
if (block != Blocks.Air)
{
rightmostWall = Math.Max(rightmostWall, checkX + 1); // Right side of the block
}
}
}
return rightmostWall != float.MinValue ? rightmostWall : box.Min.X;
}
private float GetRightWallPosition(AABB box)
{
int minY = (int)MathF.Floor(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y);
int minZ = (int)MathF.Floor(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z);
int checkX = (int)MathF.Floor(box.Max.X);
float leftmostWall = float.MaxValue;
for (int y = minY; y <= maxY; y++)
{
for (int z = minZ; z <= maxZ; z++)
{
Blocks block = _world.GetBlock(checkX, y, z);
if (block != Blocks.Air)
{
leftmostWall = Math.Min(leftmostWall, checkX); // Left side of the block
}
}
}
return leftmostWall != float.MaxValue ? leftmostWall : box.Max.X;
}
private float GetBackWallPosition(AABB box)
{
int minX = (int)MathF.Floor(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X);
int minY = (int)MathF.Floor(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y);
int checkZ = (int)MathF.Floor(box.Min.Z);
float frontmostWall = float.MinValue;
for (int x = minX; x <= maxX; x++)
{
for (int y = minY; y <= maxY; y++)
{
Blocks block = _world.GetBlock(x, y, checkZ);
if (block != Blocks.Air)
{
frontmostWall = Math.Max(frontmostWall, checkZ + 1); // Front side of the block
}
}
}
return frontmostWall != float.MinValue ? frontmostWall : box.Min.Z;
}
private float GetFrontWallPosition(AABB box)
{
int minX = (int)MathF.Floor(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X);
int minY = (int)MathF.Floor(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y);
int checkZ = (int)MathF.Floor(box.Max.Z);
float backmostWall = float.MaxValue;
for (int x = minX; x <= maxX; x++)
{
for (int y = minY; y <= maxY; y++)
{
Blocks block = _world.GetBlock(x, y, checkZ);
if (block != Blocks.Air)
{
backmostWall = Math.Min(backmostWall, checkZ); // Back side of the block
}
}
}
return backmostWall != float.MaxValue ? backmostWall : box.Max.Z;
}
public bool HasCollision(AABB box)
{
int minX = (int)MathF.Floor(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X);
int minY = (int)MathF.Floor(box.Min.Y);
int maxY = (int)MathF.Floor(box.Max.Y);
int minZ = (int)MathF.Floor(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z);
for (int x = minX; x <= maxX; x++)
{
for (int y = minY; y <= maxY; y++)
{
for (int z = minZ; z <= maxZ; z++)
{
Blocks block = _world.GetBlock(x, y, z);
if (block != Blocks.Air)
{
return true;
}
}
}
}
return false;
}
public void ApplyImpulse(Vector3 force)
{
Velocity += force;
}
public AABB GetBoundingBox()
{
return GetBoundingBoxAt(Position);
}
public AABB GetBoundingBoxAt(Vector3 position)
{
float halfWidth = Width / 2;
float halfHeight = Height / 2;
Vector3 min = new Vector3(
position.X - halfWidth,
position.Y - halfHeight, // Center Y minus half height
position.Z - halfWidth
);
Vector3 max = new Vector3(
position.X + halfWidth,
position.Y + halfHeight, // Center Y plus half height
position.Z + halfWidth
);
return new AABB(min, max);
}
public void UpdateOnGround()
{
AABB box = GetBoundingBox();
float yCheck = box.Min.Y - 0.05f;
int minX = (int)MathF.Floor(box.Min.X);
int maxX = (int)MathF.Floor(box.Max.X);
int minZ = (int)MathF.Floor(box.Min.Z);
int maxZ = (int)MathF.Floor(box.Max.Z);
OnGround = false;
for (int x = minX; x <= maxX; x++)
{
for (int z = minZ; z <= maxZ; z++)
{
Blocks block = _world.GetBlock(x, (int)MathF.Floor(yCheck), z);
if (block != Blocks.Air)
{
OnGround = true;
return;
}
}
}
}
}
}

26
FaceData.cs Normal file
View File

@@ -0,0 +1,26 @@
using System.Runtime.InteropServices;
namespace Voxel
{
public struct FaceData
{
public Orientation Facing;
public byte X, Y, Z;
public Textures Texture;
public byte LightLevel;
public byte[] Pack()
{
return new byte[]
{
X,
Y,
Z,
(byte)Facing,
(byte)Texture,
LightLevel,
0,0 // two bits empty
};
}
}
}

38
Input.cs Normal file
View File

@@ -0,0 +1,38 @@
using OpenTK.Windowing.Common;
using OpenTK.Windowing.GraphicsLibraryFramework;
namespace Voxel
{
public static class Input
{
private static Dictionary<Keys, bool> _keystates = new Dictionary<Keys, bool>();
private static Dictionary<MouseButton, bool> _mouseButtonStates = new Dictionary<MouseButton, bool>();
public static Action<MouseWheelEventArgs> OnMouseWheel;
public static bool GetMouseButton(MouseButton button)
{
return _mouseButtonStates.TryGetValue(button, out bool pressed) && pressed;
}
public static void SetMouseButton(MouseButton button, bool pressed)
{
_mouseButtonStates[button] = pressed;
}
public static bool GetKey(Keys key)
{
return _keystates.TryGetValue(key, out bool pressed) && pressed;
}
public static void SetKey(Keys key, bool pressed)
{
_keystates[key] = pressed;
}
public static void MouseWheel(MouseWheelEventArgs e)
{
OnMouseWheel.Invoke(e);
}
}
}

2506
Noise/FastNoiseLite.cs Normal file

File diff suppressed because it is too large Load Diff

162
Player.cs Normal file
View File

@@ -0,0 +1,162 @@
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.GraphicsLibraryFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace Voxel
{
public class Player : Entity
{
public double lastClick = 0;
public readonly float mouseCooldown = 0.2f;
private int _blockIndex = 0;
private double _tickTime = 0;
private Blocks _selectedBlock = Blocks.OakPlanks;
private Vector3 previousPosition;
public Player(Vector3 startPos, World world) : base(startPos, 0.5f, 1.8f, world)
{
Input.OnMouseWheel += SwitchBlock;
}
public void PlaceBlock()
{
var (success, hit, x, y, z, normal) = _world.Raycast(Camera.Position, Camera.Front.Normalized(), 8);
if (!success) return;
x += normal.X;
y += normal.Y;
z += normal.Z;
_world.SetBlock(x, y, z, _selectedBlock);
}
public void BreakBlock()
{
var (success, hit, x, y, z, normal) = _world.Raycast(Camera.Position, Camera.Front.Normalized(), 8);
if (!success) return;
_world.SetBlock(x, y, z, Blocks.Air);
}
public new void Tick()
{
previousPosition = Position;
float forwards = 0;
float sidewards = 0;
if (Input.GetKey(Keys.W))
forwards = -1;
if (Input.GetKey(Keys.S))
forwards = 1;
if (Input.GetKey(Keys.A))
sidewards = -1;
if (Input.GetKey(Keys.D))
sidewards = 1;
if (Input.GetKey(Keys.Space) && OnGround)
{
Console.WriteLine("Jump");
Velocity = new Vector3(Velocity.X, 0.42f, Velocity.Z);
OnGround = false;
}
if (Input.GetMouseButton(MouseButton.Right) && lastClick == 0)
{
lastClick = mouseCooldown;
PlaceBlock();
}
if (Input.GetMouseButton(MouseButton.Left) && lastClick == 0)
{
lastClick = mouseCooldown;
BreakBlock();
}
ApplyWalkSpeed(sidewards, forwards);
base.Tick();
}
public void Update(float deltaTime, float alpha)
{
Camera.Position = Vector3.Lerp(previousPosition, Position, alpha) + Vector3.UnitY * 0.62f;
Rotation = Camera.Yaw;
if (lastClick > 0)
{
lastClick -= deltaTime;
if (lastClick < 0) lastClick = 0;
}
if (!Input.GetMouseButton(MouseButton.Right) && !Input.GetMouseButton(MouseButton.Left))
{
lastClick = 0;
}
}
public void ApplyWalkSpeed(float x, float z)
{
Vector3 inputDir = new Vector3(x, 0, z);
if (inputDir.LengthSquared > 0)
inputDir = Vector3.Normalize(inputDir);
float yawRad = MathHelper.DegreesToRadians(Rotation);
float cos = MathF.Cos(yawRad);
float sin = MathF.Sin(yawRad);
Vector3 worldDir = new Vector3(
inputDir.X * cos - inputDir.Z * sin,
0,
inputDir.X * sin + inputDir.Z * cos
);
float M_t = 1.4f;
float groundMultiplier = 0.6f;
if (!OnGround)
{
Vector3 airAccel = worldDir * 0.02f * M_t;
Velocity = new Vector3(Velocity.X + airAccel.X, Velocity.Y, Velocity.Z + airAccel.Z);
}
else
{
Vector3 groundAccel = worldDir * 0.1f * M_t;
Velocity = new Vector3(Velocity.X * groundMultiplier + groundAccel.X,
Velocity.Y,
Velocity.Z * groundMultiplier + groundAccel.Z);
}
}
public void SwitchBlock(MouseWheelEventArgs e)
{
var keys = BlockDefinitions.Blocks.Keys.ToList();
bool inverted = false;
if (e.OffsetY < 0)
inverted = true;
if (inverted)
if (_blockIndex == 0)
_blockIndex = keys.Count -1;
else
_blockIndex -= 1;
else
_blockIndex += 1;
_blockIndex = _blockIndex % keys.Count;
_selectedBlock = keys[_blockIndex];
Console.WriteLine(_selectedBlock);
}
}
}

View File

@@ -1,4 +1,5 @@
using Voxel;
using OpenTK.Mathematics;
using Voxel;
internal class Program
{
@@ -8,7 +9,46 @@ internal class Program
int sizeY = 600;
string title = "Game";
World world = new World();
Window window = new Window(sizeX, sizeY, title);
Console.WriteLine("Generating map...");
int worldSizeX = 8;
int worldSizeY = 8;
float maxI = worldSizeX * worldSizeY;
int i = 0;
int lastPercentage = 0;
for (int x = 0; x < worldSizeX; x++)
{
for (int y = 0; y < worldSizeY; y++)
{
i++;
Chunk chunk = new Chunk(x, y);
world.AddChunk(chunk);
int percentage = (int)((i / maxI) * 100);
if (percentage > lastPercentage)
{
lastPercentage = percentage;
Console.WriteLine((percentage).ToString() + "%");
}
}
}
Console.WriteLine("Generated " + maxI.ToString() + " chunks");
Renderer.SetWorld(world);
Vector3 startPos = new Vector3(15, 64, 15);
Player player = new Player(startPos, world);
window.Tick += player.Tick;
window.Update += player.Update;
window.Update += Camera.UpdateFOV;
window.Run();
}
}

136
Renderer.cs Normal file
View File

@@ -0,0 +1,136 @@
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Voxel
{
static class Renderer
{
private static int _ssbo;
private static int _vao;
private static bool _buffersDirty;
private static Dictionary<(int, int), int> _chunkBufferSizes = new Dictionary<(int, int), int>();
private static Shader _shader;
private static readonly Texture _texture;
private static World? _world;
static Renderer()
{
string vertexPath = "Shaders/shader.vert";
string fragmentPath = "Shaders/shader.frag";
string texturePath = "atlas.png";
_shader = new Shader(vertexPath, fragmentPath);
_texture = new Texture(texturePath);
_shader.SetInt("uTexture", 0);
_ssbo = GL.GenBuffer();
_vao = GL.GenVertexArray();
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ShaderStorageBuffer, _ssbo);
GL.BufferData(BufferTarget.ShaderStorageBuffer, 1024 * 1024 * 128, IntPtr.Zero, BufferUsageHint.DynamicDraw);
GL.BindBufferBase(BufferRangeTarget.ShaderStorageBuffer, 0, _ssbo);
}
public static void Render()
{
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ShaderStorageBuffer, _ssbo);
_shader.Use();
_shader.SetMatrix4("view", Camera.view);
_shader.SetVector3("cameraPosition", Camera.Position);
_shader.SetMatrix4("projection", Camera.projection);
if (_buffersDirty)
{
UpdateAllChunksBuffer();
_buffersDirty = false;
}
RenderWorld();
RenderUi();
}
private static void UpdateAllChunksBuffer()
{
if (_world == null) return;
int offset = 0;
foreach (Chunk chunk in _world.GetAllChunks())
{
ChunkMesh chunkMesh = chunk.GetChunkMesh();
if (chunkMesh.NeedsUpdate)
{
byte[] data = chunkMesh.GetPackedData();
GL.BufferSubData(BufferTarget.ShaderStorageBuffer, (IntPtr)offset * 8, chunkMesh.Size * 8, data);
}
_chunkBufferSizes[(chunk.X, chunk.Y)] = offset;
offset += chunkMesh.Size * 8;
}
}
private static void RenderUi()
{
GL.Disable(EnableCap.DepthTest);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
//_uiShader.Use();
//Matrix4 projection = Matrix4.CreateOrthographicOffCenter(
// 0, screenWidth, screenHeight, 0, -1, 1);
//_uiShader.SetMatrix4("projection", projection);
// Bind UI texture atlas
//_uiTexture.Bind();
// Draw all UI sprites (batch by texture for efficiency)
//foreach (var sprite in _uiSprites)
//{
//sprite.Draw();
//}
// Restore 3D settings
GL.Disable(EnableCap.Blend);
GL.Enable(EnableCap.DepthTest);
}
private static void RenderWorld()
{
if (_world == null) return;
foreach (Chunk chunk in _world.GetAllChunks())
{
ChunkMesh chunkMesh = chunk.GetChunkMesh();
if (chunkMesh.Size == 0) continue;
if (!_chunkBufferSizes.TryGetValue((chunk.X, chunk.Y), out int offset)) continue;
_shader.SetInt("chunkX", chunk.X);
_shader.SetInt("chunkY", chunk.Y);
//GL.MemoryBarrier(MemoryBarrierFlags.ShaderStorageBarrierBit);
GL.DrawArrays(PrimitiveType.Triangles, offset * 6, chunkMesh.Size * 6);
}
}
public static void SetWorld(World world)
{
_world = world;
_buffersDirty = true;
}
public static void MarkBuffersDirty()
{
_buffersDirty = true;
}
}
}

View File

@@ -1,16 +1,11 @@
using OpenTK.Graphics.OpenGL4;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
using OpenTK.Mathematics;
namespace Voxel
{
public class Shader
{
int handle;
private int _handle;
private bool disposedValue = false;
public Shader(string vertexPath, string fragmentPath)
@@ -41,36 +36,69 @@ namespace Voxel
// attach
handle = GL.CreateProgram();
_handle = GL.CreateProgram();
GL.AttachShader(handle, vertexShader);
GL.AttachShader(handle, fragmentShader);
GL.AttachShader(_handle, vertexShader);
GL.AttachShader(_handle, fragmentShader);
GL.LinkProgram(handle);
GL.LinkProgram(_handle);
GL.GetProgram(handle, GetProgramParameterName.LinkStatus, out success);
GL.GetProgram(_handle, GetProgramParameterName.LinkStatus, out success);
if (success == 0)
{
string infoLog = GL.GetProgramInfoLog(handle);
string infoLog = GL.GetProgramInfoLog(_handle);
Console.WriteLine(infoLog);
}
GL.DetachShader(handle, vertexShader);
GL.DetachShader(handle, fragmentShader);
GL.DetachShader(_handle, vertexShader);
GL.DetachShader(_handle, fragmentShader);
GL.DeleteShader(fragmentShader);
GL.DeleteShader(vertexShader);
}
public void SetMatrix4(string name, Matrix4 matrix)
{
int location = GL.GetUniformLocation(_handle, name);
if (location == -1)
{
Console.WriteLine($"Uniform '{name}' not found in shader.");
return;
}
GL.UniformMatrix4(location, false, ref matrix);
}
public void SetVector3(string name, Vector3 vector3)
{
int location = GL.GetUniformLocation(_handle, name);
if (location == -1)
{
Console.WriteLine($"Uniform '{name}' not found in shader.");
return;
}
GL.Uniform3(location, ref vector3);
}
public void SetInt(string name, int value)
{
int location = GL.GetUniformLocation(_handle, name);
if (location == -1)
{
Console.WriteLine($"Uniform '{name}' not found in shader.");
return;
}
GL.Uniform1(location, value);
}
public void Use()
{
GL.UseProgram(handle);
GL.UseProgram(_handle);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
GL.DeleteProgram(handle);
GL.DeleteProgram(_handle);
disposedValue = true;
}

View File

@@ -1,9 +1,25 @@
#version 330 core
out vec4 FragColor;
#version 430 core
in vec4 vertexColor;
out vec4 FragColor;
in vec2 fragUV;
in vec3 fragPos;
in float lighting;
uniform sampler2D uTexture;
uniform vec3 cameraPosition;
void main()
{
FragColor = vertexColor;
float fogEnd = 512;
float fogStart = 32;
float dist = length(cameraPosition - fragPos);
float fogFactor = (fogEnd - dist) / (fogEnd - fogStart);
fogFactor = clamp(fogFactor, 0, 1);
vec4 fogColor = vec4(0.8,0.8,0.8,1);
vec4 texColor = texture(uTexture, fragUV) * lighting;
vec4 color = mix(fogColor, texColor, fogFactor);
FragColor = vec4(color.rgb, texColor.a);
}

View File

@@ -1,10 +1,117 @@
#version 330 core
layout (location = 0) in vec3 aPosition;
#version 430 core
out vec4 vertexColor;
struct FaceData {
uint x;
uint y;
uint z;
uint facing; // 0=+X,1=-X,2=+Y,3=-Y,4=+Z,5=-Z
uint texture;
uint lightLevel;
};
layout(std430, binding = 0) buffer FaceBuffer {
uint faces[];
};
uniform mat4 view;
uniform mat4 projection;
uniform int chunkX;
uniform int chunkY;
out vec2 fragUV;
out float lighting;
out vec3 fragPos;
const float lightMult[6] = float[6](0.6, 0.6, 1.0, 0.5, 0.8, 0.8);
const vec3 offsets[6][6] = vec3[6][6](
vec3[6]( // +X
vec3(1, 0, 0),
vec3(1, 1, 1),
vec3(1, 0, 1),
vec3(1, 1, 1),
vec3(1, 0, 0),
vec3(1, 1, 0)
),
vec3[6]( // -X
vec3(0, 0, 1),
vec3(0, 1, 0),
vec3(0, 0, 0),
vec3(0, 1, 0),
vec3(0, 0, 1),
vec3(0, 1, 1)
),
vec3[6]( // +Y
vec3(0, 1, 0),
vec3(1, 1, 1),
vec3(1, 1, 0),
vec3(1, 1, 1),
vec3(0, 1, 0),
vec3(0, 1, 1)
),
vec3[6]( // -Y
vec3(0, 0, 1),
vec3(1, 0, 0),
vec3(1, 0, 1),
vec3(1, 0, 0),
vec3(0, 0, 1),
vec3(0, 0, 0)
),
vec3[6]( // +Z
vec3(1, 0, 1),
vec3(0, 1, 1),
vec3(0, 0, 1),
vec3(0, 1, 1),
vec3(1, 0, 1),
vec3(1, 1, 1)
),
vec3[6]( // -Z
vec3(0, 0, 0),
vec3(1, 1, 0),
vec3(1, 0, 0),
vec3(1, 1, 0),
vec3(0, 0, 0),
vec3(0, 1, 0)
)
);
const vec2 uvs[6] = vec2[6](vec2(0,0), vec2(1,1), vec2(1,0), vec2(1,1), vec2(0,0), vec2(0,1));
void main()
{
gl_Position = vec4(aPosition, 1.0);
vertexColor = vec4(aPosition + vec3(0.5,0.5,0.5), 1.0);
uint faceIndex = gl_VertexID / 6u;
uint vertIndex = gl_VertexID % 6u;
uint start = faceIndex * 2u; // 2 byte per face
uint u0 = faces[start]; // data in uint 0
uint u1 = faces[start + 1]; // data in uint 1
// extract values from bits
uint x = u0 & 0xFFu;
uint y = (u0 >> 8) & 0xFFu;
uint z = (u0 >> 16) & 0xFFu;
uint facing = (u0 >> 24) & 0xFFu;
uint texture = u1 & 0xFFu;
uint lightLevel = (u1 >> 8) & 0xFFu;
vec3 basePos = vec3(x + chunkX * 16, y, z + chunkY * 16);
vec4 worldPos = vec4(basePos + offsets[facing][vertIndex], 1.0);
float light = float(lightLevel) / 255.0; // use later for caves and stuff
// UV mapping
uint col = texture & 15u; // texture % 16
uint row = texture >> 4; // texture / 16
row = 15u - row; // invert row so 0 is top
// convert to float after int math, divide by 16
vec2 uv = uvs[vertIndex] * 0.0625;
uv.x += float(col) * 0.0625;
uv.y += float(row) * 0.0625;
fragUV = uv;
lighting = lightMult[facing];
fragPos = vec3(worldPos.x, worldPos.y, worldPos.z);
gl_Position = projection * view * worldPos;
}

43
Texture.cs Normal file
View File

@@ -0,0 +1,43 @@
using OpenTK.Graphics.OpenGL4;
using StbImageSharp;
namespace Voxel
{
public class Texture
{
private int _handle;
private string _path;
public Texture(string path)
{
_handle = GL.GenTexture();
_path = path;
LoadFromFile();
}
private void LoadFromFile()
{
StbImage.stbi_set_flip_vertically_on_load(1);
ImageResult image = ImageResult.FromStream(File.OpenRead(_path), ColorComponents.RedGreenBlueAlpha);
GL.BindTexture(TextureTarget.Texture2D, _handle);
GL.TexImage2D(
TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
image.Width, image.Height, 0,
PixelFormat.Rgba, PixelType.UnsignedByte, image.Data
);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
}
public void Bind(TextureUnit unit = TextureUnit.Texture0)
{
GL.ActiveTexture(unit);
GL.BindTexture(TextureTarget.Texture2D, _handle);
}
}
}

44
Textures.cs Normal file
View File

@@ -0,0 +1,44 @@
namespace Voxel
{
public enum Textures : uint
{
GrassTop,
Stone,
Dirt,
GrassSide,
OakPlanks,
SmoothStoneSlab,
SmoothStone,
Bricks,
TntSide,
TntTop,
TntBottom,
Cobweb,
Rose,
Dandelion,
Water,
OakSapling,
Cobblestone,
Bedrock,
Sand,
Gravel,
OakSide,
OakTop,
IronBlock,
GoldBlock,
DiamondBlock,
EmeraldBlock,
RedstoneBlock,
none0,
RedMushroom,
BrownMushroom,
JungleSapling,
none1,
GoldOre,
IronOre,
CoalOre,
Bookshelf,
MossyCobblestone,
Obsidian,
}
}

View File

@@ -1,48 +0,0 @@
using OpenTK.Graphics.OpenGL4;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Voxel
{
public class Triangle
{
private int _vao;
private Shader _shader;
public Triangle()
{
string vertexPath = "Shaders/shader.vert";
string fragmentPath = "Shaders/shader.frag";
_shader = new Shader(vertexPath, fragmentPath);
float[] vertices =
{
-0.5f, -0.5f, 0.0f, //top
0.5f, -0.5f, 0.0f, // bottom right
0.0f, 0.5f, 0.0f // bottom left
};
_vao = GL.GenVertexArray();
GL.BindVertexArray(_vao);
int vbo = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
GL.EnableVertexAttribArray(0);
_shader.Use();
}
public void Draw()
{
GL.BindVertexArray(_vao);
GL.DrawArrays(PrimitiveType.Triangles, 0, 3);
}
}
}

17
UISprite.cs Normal file
View File

@@ -0,0 +1,17 @@
using OpenTK.Mathematics;
using System.Drawing;
public class UISprite
{
public Vector2 Position; // Screen pixels (not normalized)
public Vector2 Size; // Size in pixels
public Rectangle TextureRegion; // Which part of atlas to use
public Color Tint = Color.White;
public float Rotation = 0f;
public Vector2 Origin = Vector2.Zero; // Rotation/scale origin
public void Draw()
{
// Draw textured quad using TextureRegion coordinates
}
}

View File

@@ -9,9 +9,13 @@
<ItemGroup>
<PackageReference Include="OpenTK" Version="4.9.4" />
<PackageReference Include="StbImageSharp" Version="2.30.15" />
</ItemGroup>
<ItemGroup>
<None Update="atlas.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Shaders\shader.frag">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>

117
Window.cs
View File

@@ -1,30 +1,70 @@
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Common.Input;
using OpenTK.Windowing.Desktop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTK.Windowing.GraphicsLibraryFramework;
namespace Voxel
{
public class Window(int width, int height, string title) : GameWindow(GameWindowSettings.Default, new NativeWindowSettings() { Size = (width, height), Title = title })
public class Window(int width, int height, string title) : GameWindow(GameWindowSettings.Default, new NativeWindowSettings() { ClientSize = (width, height), Title = title })
{
private Triangle _triangle;
public readonly int Width = width;
public readonly int Height = height;
public uint frames = 0;
public double timeElapsed = 0;
protected override void OnUpdateFrame(FrameEventArgs args)
public event Action<float, float> Update;
public event Action Tick;
private double _tickTime;
public const double TICK_LENGTH = 1.0 / 20.0; // 20 TPS
protected override void OnUpdateFrame(FrameEventArgs e)
{
base.OnUpdateFrame(args);
base.OnUpdateFrame(e);
_tickTime += e.Time;
while (_tickTime >= TICK_LENGTH)
{
_tickTime -= TICK_LENGTH;
Tick(); // run exactly once per tick
}
if (Input.GetKey(Keys.Escape))
{
Close();
}
if (Input.GetKey(Keys.F11))
{
if (!IsFullscreen)
WindowState = WindowState.Fullscreen;
else
WindowState = WindowState.Normal;
}
}
protected override void OnRenderFrame(FrameEventArgs args)
protected override void OnRenderFrame(FrameEventArgs e)
{
base.OnRenderFrame(args);
base.OnRenderFrame(e);
GL.Clear(ClearBufferMask.ColorBufferBit);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
_triangle.Draw();
frames++;
timeElapsed += e.Time;
float alpha = (float)(_tickTime / Window.TICK_LENGTH);
float deltaTime = (float)e.Time;
if (timeElapsed >= 1)
{
Console.WriteLine("FPS: " + frames.ToString());
timeElapsed = 0;
frames = 0;
}
Update.Invoke(deltaTime, alpha);
Renderer.Render();
SwapBuffers();
}
@@ -33,6 +73,8 @@ namespace Voxel
{
base.OnFramebufferResize(e);
Camera.UpdateSize(e.Width, e.Height);
GL.Viewport(0, 0, e.Width, e.Height);
}
@@ -40,9 +82,54 @@ namespace Voxel
{
base.OnLoad();
_triangle = new Triangle();
GL.ClearColor(0.72f, 0.88f, 0.97f, 1f);
GL.ClearColor(0.5f, 0.5f, 0.5f, 1f);
GL.Enable(EnableCap.CullFace);
GL.Enable(EnableCap.DepthTest);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
CursorState = CursorState.Grabbed;
VSync = VSyncMode.On;
Camera.UpdateSize(Width, Height);
}
protected override void OnMouseMove(MouseMoveEventArgs e)
{
base.OnMouseMove(e);
Camera.UpdateMouse(e.Delta);
}
protected override void OnKeyUp(KeyboardKeyEventArgs e)
{
base.OnKeyUp(e);
Input.SetKey(e.Key, false);
}
protected override void OnKeyDown(KeyboardKeyEventArgs e)
{
base.OnKeyDown(e);
Input.SetKey(e.Key, true);
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
Input.SetMouseButton(e.Button, true);
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
Input.SetMouseButton(e.Button, false);
}
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
base.OnMouseWheel(e);
Input.MouseWheel(e);
}
}
}

185
World.cs Normal file
View File

@@ -0,0 +1,185 @@
using OpenTK.Mathematics;
using System.Collections.Generic;
using System.Drawing;
namespace Voxel
{
public class World
{
private Dictionary<(int, int), Chunk> _chunks;
public World()
{
_chunks = new Dictionary<(int, int), Chunk>();
}
public Chunk GetChunk(int chunkX, int chunkZ)
{
_chunks.TryGetValue((chunkX, chunkZ), out Chunk chunk);
return chunk;
}
public void AddChunk(Chunk chunk)
{
_chunks[(chunk.X, chunk.Y)] = chunk;
Dictionary<Orientation, Orientation> oppositeOrientation = new()
{
{Orientation.West, Orientation.East},
{Orientation.East, Orientation.West},
{Orientation.North, Orientation.South},
{Orientation.South, Orientation.North},
};
foreach (var (orientation, neighbor) in GetChunkNeighbors(chunk))
{
neighbor.Neighbors[oppositeOrientation[orientation]] = chunk;
chunk.Neighbors[orientation] = neighbor;
neighbor.UpdateChunkMesh();
}
chunk.UpdateChunkMesh();
}
public void RemoveChunk(int chunkX, int chunkZ)
{
_chunks.Remove((chunkX, chunkZ));
}
public IEnumerable<Chunk> GetAllChunks()
{
return _chunks.Values;
}
public Blocks GetBlock(int worldX, int worldY, int worldZ)
{
int chunkX = worldX / Chunk.Size;
int chunkZ = worldZ / Chunk.Size;
Chunk chunk = GetChunk(chunkX, chunkZ);
if (chunk == null) return 0; // air if chunk not loaded
int localX = worldX % Chunk.Size;
int localZ = worldZ % Chunk.Size;
return chunk.GetBlock(localX, worldY, localZ);
}
List<Orientation> GetEdgeOrientations(int localX, int localZ, int size)
{
var orientations = new List<Orientation>();
if (localX == size - 1) orientations.Add(Orientation.West);
if (localX == 0) orientations.Add(Orientation.East);
if (localZ == size - 1) orientations.Add(Orientation.North);
if (localZ == 0) orientations.Add(Orientation.South);
return orientations;
}
public void SetBlock(int worldX, int worldY, int worldZ, Blocks block)
{
int chunkX = worldX / Chunk.Size;
int chunkZ = worldZ / Chunk.Size;
Chunk chunk = GetChunk(chunkX, chunkZ);
if (chunk == null) return;
int localX = worldX % Chunk.Size;
int localZ = worldZ % Chunk.Size;
chunk.SetBlock(localX, worldY, localZ, block);
if (block == Blocks.Air && (localX == 15 || localX == 0) || (localZ == 15 || localZ == 0))
{
foreach (var orientation in GetEdgeOrientations(localX, localZ, Chunk.Size))
{
if (chunk.Neighbors.TryGetValue(orientation, out var neighbor) && neighbor != null)
{
neighbor.UpdateChunkMesh();
}
}
}
}
public IEnumerable<(Orientation orientation, Chunk neighbor)> GetChunkNeighbors(Chunk chunk)
{
Dictionary<Orientation, (int x, int y)> offsets = new()
{
{ Orientation.West, (1, 0) },
{ Orientation.East, (-1, 0) },
{ Orientation.North, (0, 1) },
{ Orientation.South, (0, -1) }
};
foreach (var kv in offsets)
{
int nx = chunk.X + kv.Value.x;
int ny = chunk.Y + kv.Value.y;
Chunk neighbor = GetChunk(nx, ny);
if (neighbor != null)
yield return (kv.Key, neighbor);
}
}
public (bool success, Blocks block, int x, int y, int z, Vector3i normal) Raycast(Vector3 origin, Vector3 direction, float maxDistance)
{
int x = (int)MathF.Floor(origin.X);
int y = (int)MathF.Floor(origin.Y);
int z = (int)MathF.Floor(origin.Z);
int stepX = direction.X > 0 ? 1 : -1;
int stepY = direction.Y > 0 ? 1 : -1;
int stepZ = direction.Z > 0 ? 1 : -1;
float tDeltaX = direction.X != 0 ? MathF.Abs(1 / direction.X) : float.MaxValue;
float tDeltaY = direction.Y != 0 ? MathF.Abs(1 / direction.Y) : float.MaxValue;
float tDeltaZ = direction.Z != 0 ? MathF.Abs(1 / direction.Z) : float.MaxValue;
float tMaxX = direction.X > 0 ? (MathF.Floor(origin.X) + 1 - origin.X) * tDeltaX
: (origin.X - MathF.Floor(origin.X)) * tDeltaX;
float tMaxY = direction.Y > 0 ? (MathF.Floor(origin.Y) + 1 - origin.Y) * tDeltaY
: (origin.Y - MathF.Floor(origin.Y)) * tDeltaY;
float tMaxZ = direction.Z > 0 ? (MathF.Floor(origin.Z) + 1 - origin.Z) * tDeltaZ
: (origin.Z - MathF.Floor(origin.Z)) * tDeltaZ;
float distance = 0f;
Vector3i normal = Vector3i.Zero;
while (distance <= maxDistance)
{
Blocks block = GetBlock(x, y, z);
if (block != Blocks.Air)
return (true, block, x, y, z, normal);
if (tMaxX < tMaxY && tMaxX < tMaxZ)
{
x += stepX;
normal = new Vector3i(-stepX, 0, 0);
distance = tMaxX;
tMaxX += tDeltaX;
}
else if (tMaxY < tMaxZ)
{
y += stepY;
normal = new Vector3i(0, -stepY, 0);
distance = tMaxY;
tMaxY += tDeltaY;
}
else
{
z += stepZ;
normal = new Vector3i(0, 0, -stepZ);
distance = tMaxZ;
tMaxZ += tDeltaZ;
}
}
return (false, Blocks.Air, 0, 0, 0, Vector3i.Zero);
}
public void Update()
{
}
}
}

79
Worldgen.cs Normal file
View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Voxel
{
public static class Worldgen
{
private static int baseHeight = 32;
private static float res1 = 2;
private static float res2 = 4;
private static float amplitude1 = 4;
private static float amplitude2 = 2;
private static float elevationMapRes = (float)1/8;
private static float elevationMapAmplitude = 32;
private static Random random = new Random();
private static FastNoiseLite noise = new FastNoiseLite(random.Next());
public static int GetHeight(int x, int z)
{
float map1 = noise.GetNoise(
x * res1,
z * res1
) * amplitude1;
float map2 = noise.GetNoise(x * res2,
z * res2
) * amplitude2;
float elevationMap = (noise.GetNoise(
x * elevationMapRes,
z * elevationMapRes
) + 0.25f) * elevationMapAmplitude;
return baseHeight + (int)(elevationMap + map1 + map2);
}
public static Blocks GetBlock(int y, int maxY)
{
if (y > maxY)
{
return Blocks.Air;
}
if (y == maxY)
{
if (y < baseHeight)
return Blocks.Sand;
else
return Blocks.Grass;
}
if (y < 4)
{
if (y == 0) return Blocks.Bedrock;
float randomValue = random.NextSingle();
if (randomValue < (1f / y))
{
return Blocks.Bedrock;
}
return Blocks.Stone;
}
if (y > maxY - 6)
{
return Blocks.Dirt;
}
return Blocks.Stone;
}
}
}

BIN
atlas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB