commit f4cfde8cf9301642cbdd93a1c1250dc122d8b81f Author: Jake Date: Mon May 26 20:59:26 2025 -0400 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b06880 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# voxelThing +Jake's Raylib Minecraft Clone + +Implements a voxel chunk renderer. Chunks are stored as a 3D array of block structs elements which define block types and later other block data. The chunk is converted to a mesh such that only faces exposed to air blocks or the chunk boarders are rendered. + +Code is rough and messy and not much is implemented yet. + +Depends on Raylib. + +## Build Instructions: + +1) Install Raylib. +2) Clone the repo. +3) Build by running make. + diff --git a/assets/TextureAtlas.png b/assets/TextureAtlas.png new file mode 100644 index 0000000..50cca6d Binary files /dev/null and b/assets/TextureAtlas.png differ diff --git a/assets/TextureAtlas.xcf b/assets/TextureAtlas.xcf new file mode 100644 index 0000000..a416b97 Binary files /dev/null and b/assets/TextureAtlas.xcf differ diff --git a/include/atlasDefinitions.h b/include/atlasDefinitions.h new file mode 100644 index 0000000..a2ba30b --- /dev/null +++ b/include/atlasDefinitions.h @@ -0,0 +1,18 @@ +// atlasDefinitions.h +// This header file contians define statements linking the names of each texture in the atlas to its index. +// I feel like there's probably a better way to link these things together, but this will work for now.' + +#ifndef ATLAS_DEFINITIONS_H +#define ATLAS_DEFINITIONS_H + +#define TILE_STONE 0 +#define TILE_DIRT 1 +#define TILE_GRASS_TOP 2 +#define TILE_GRASS_SIDE 3 +#define TILE_SAND 4 +#define TILE_GRAVEL 5 +#define TILE_LOG_TOP 6 +#define TILE_LOG_SIDE 7 +#define TILE_LEAF 8 + +#endif diff --git a/include/blockTypes.h b/include/blockTypes.h new file mode 100644 index 0000000..36eb485 --- /dev/null +++ b/include/blockTypes.h @@ -0,0 +1,23 @@ +// blockTypes.h +#ifndef BLOCK_TYPES_H +#define BLOCKTYPES_H + +// Definitions for Block IDs (notes are texture atlas indicies) +#define BLOCK_AIR 0 // No texture. +#define BLOCK_STONE 1 // 0 +#define BLOCK_DIRT 2 // 1 +#define BLOCK_GRASS 3 // top, 2, sides 3, bottom 1 +#define BLOCK_SAND 4 // 4 +#define BLOCK_GRAVEL 5 // 5 +#define BLOCK_LOG 6 // top 6, sides 7, bottom 6 +#define BLOCK_LEAF 7 // 8 + +typedef struct { + int id; + const char *name; + int faceTiles[6]; // Index by face: 0=-X, 1=+X, 2=-Y, 3=+Y, 4=-Z, 5=+Z +} BlockType; + +const BlockType *GetBlockType(int blockID); + +#endif diff --git a/include/chunkGenerator.h b/include/chunkGenerator.h new file mode 100644 index 0000000..55fac21 --- /dev/null +++ b/include/chunkGenerator.h @@ -0,0 +1,16 @@ +// Chunk generation functions for voxelThing. +#ifndef CHUNK_GENERATOR_H +#define CHUNK_GENERATOR_H + +#include "chunkStructures.h" + +// Function for initializing a chunk as a typical sort of flatworld chunk. +void GenerateFlatChunk(Chunk *chunk) ; + +// Function to initialize the chunk with dirt. +void GenerateChunk(Chunk *chunk); + +// Function for initializing a chunk sparsely. (still dirt) +void GenerateSparseChunk(Chunk *chunk); + +#endif diff --git a/include/chunkRenderer.h b/include/chunkRenderer.h new file mode 100644 index 0000000..c3b8db0 --- /dev/null +++ b/include/chunkRenderer.h @@ -0,0 +1,11 @@ +// chunkRenderer.h +#ifndef CHUNK_RENDERER_H +#define CHUNK_RENDERER_H + +#include "raylib.h" +#include "chunkStructures.h" + +Mesh GenerateChunkMesh(Chunk *chunk); +Vector2 GetTileUV(int tileIndex, int corner); + +#endif diff --git a/include/chunkStructures.h b/include/chunkStructures.h new file mode 100644 index 0000000..c4d4a37 --- /dev/null +++ b/include/chunkStructures.h @@ -0,0 +1,36 @@ +// chunkStructures.h + +#ifndef CHUNK_STRUCTURES_H +#define CHUNK_STRUCTURES_H + +#include "raylib.h" + +#define CHUNK_SIZE_X 16 +#define CHUNK_SIZE_Y 16 +#define CHUNK_SIZE_Z 16 + + +typedef struct { + int type; // 0 = air, 1 = dirt, etc. +} Block; + +// 6 directions for checking neighbors: +/-X, +/-Y, +/-Z +static const int faceOffsets[6][3] = { + { -1, 0, 0 }, // left + { 1, 0, 0 }, // right + { 0, -1, 0 }, // bottom + { 0, 1, 0 }, // top + { 0, 0, -1 }, // back + { 0, 0, 1 } // front +}; + +typedef struct { + Block blocks[CHUNK_SIZE_X][CHUNK_SIZE_Y][CHUNK_SIZE_Z]; +} Chunk; + +// Function to check if a face of a block is exposed. +int IsBlockFaceExposed(Chunk *chunk, int x, int y, int z, int dir); + +void PlaceTreeAt(Chunk *chunk, int x, int y, int z) ; + +#endif diff --git a/include/playerController.h b/include/playerController.h new file mode 100644 index 0000000..46e2c69 --- /dev/null +++ b/include/playerController.h @@ -0,0 +1,22 @@ +// playerController.h +#ifndef PLAYER_CONTROLLER_H +#define PLAYER_CONTROLLER_H + +#include "raylib.h" +#include "chunkStructures.h" + +typedef struct { + bool hit; + Vector3 position; + Vector3 normal; + int blockID; + float t; +} RaycastHit; + +RaycastHit RaycastChunk(const Chunk *chunk, Vector3 origin, Vector3 direction, float maxDistance); + +void UpdateFreeCamera(Camera3D *cam, float speed, float *yawOut, float *pitchOut); + +void DrawFaceHighlight(Vector3 blockPos, Vector3 normal); + +#endif diff --git a/makefile b/makefile new file mode 100644 index 0000000..1d0d3d6 --- /dev/null +++ b/makefile @@ -0,0 +1,30 @@ +# Compiler and flags +CC = gcc +CFLAGS = -Wall -Wextra -std=c99 -O2 -Iinclude +LDFLAGS = -lraylib -lm -ldl -lpthread -lGL -lrt -lX11 + +# Directories +SRC_DIR = source +BIN_DIR = bin +BUILD_DIR = build + +# Files +TARGET = $(BIN_DIR)/voxelThing +SRC = $(wildcard $(SRC_DIR)/*.c) +OBJ = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRC)) + +# Rules +all: $(TARGET) + +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c + @mkdir -p $(BUILD_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + +$(TARGET): $(OBJ) + @mkdir -p $(BIN_DIR) + $(CC) $(OBJ) -o $@ $(LDFLAGS) + +clean: + rm -rf $(BUILD_DIR) $(BIN_DIR) + +.PHONY: all clean diff --git a/source/blockTypes.c b/source/blockTypes.c new file mode 100644 index 0000000..fa9ce4f --- /dev/null +++ b/source/blockTypes.c @@ -0,0 +1,75 @@ +// blockTypes.c +// Definitions for which blocks exist in voxelThing, as well as the tile from the atlas for each face of the block. +#include "blockTypes.h" +#include "atlasDefinitions.h" + +static const BlockType blockTable[] = { + [BLOCK_AIR] = { + .id = BLOCK_AIR, + .name = "Air", + .faceTiles = { -1, -1, -1, -1, -1, -1 } + }, + [BLOCK_STONE] = { + .id = BLOCK_STONE, + .name = "Stone", + .faceTiles = { TILE_STONE, TILE_STONE, TILE_STONE, TILE_STONE, TILE_STONE, TILE_STONE } + }, + [BLOCK_DIRT] = { + .id = BLOCK_DIRT, + .name = "Dirt", + .faceTiles = { TILE_DIRT, TILE_DIRT, TILE_DIRT, TILE_DIRT, TILE_DIRT, TILE_DIRT } + }, + [BLOCK_GRASS] = { + .id = BLOCK_GRASS, + .name = "Grass", + .faceTiles = { + TILE_GRASS_SIDE, // -X + TILE_GRASS_SIDE, // +X + TILE_DIRT, // -Y + TILE_GRASS_TOP, // +Y + TILE_GRASS_SIDE, // -Z + TILE_GRASS_SIDE // +Z + } + }, + [BLOCK_SAND] = { + .id = BLOCK_SAND, + .name = "Sand", + .faceTiles = { TILE_SAND, TILE_SAND, TILE_SAND, TILE_SAND, TILE_SAND, TILE_SAND } + }, + [BLOCK_GRAVEL] = { + .id = BLOCK_GRASS, + .name = "Grass", + .faceTiles = { + TILE_GRASS_SIDE, // -X + TILE_GRASS_SIDE, // +X + TILE_DIRT, // -Y + TILE_GRASS_TOP, // +Y + TILE_GRASS_SIDE, // -Z + TILE_GRASS_SIDE // +Z + } + }, + [BLOCK_LOG] = { + .id = BLOCK_LOG, + .name = "Log", + .faceTiles = { + TILE_LOG_SIDE, // -X + TILE_LOG_SIDE, // +X + TILE_LOG_TOP, // -Y + TILE_LOG_TOP, // +Y + TILE_LOG_SIDE, // -Z + TILE_LOG_SIDE // +Z + } + }, + [BLOCK_LEAF] = { + .id = BLOCK_LEAF, + .name = "Leaves", + .faceTiles = { TILE_LEAF, TILE_LEAF, TILE_LEAF, TILE_LEAF, TILE_LEAF, TILE_LEAF } + }, +}; + +const BlockType *GetBlockType(int blockID) { + if (blockID < 1 || blockID >= (sizeof(blockTable) / sizeof(blockTable[0]))) { + return &blockTable[BLOCK_AIR]; + } + return &blockTable[blockID]; +} diff --git a/source/chunkGenerator.c b/source/chunkGenerator.c new file mode 100644 index 0000000..49af4f4 --- /dev/null +++ b/source/chunkGenerator.c @@ -0,0 +1,25 @@ +// chunkGenerator.c +// These functions fill a chunk with blocks and are placeholders for fancier ones. + +#include "raylib.h" +#include "chunkGenerator.h" +#include "blockTypes.h" + +// Fill a chunk with normalish Minecraft style flatworld terrain. A few layers of stone on the bottom, a few layers of dirt, and a layer of grass on top. +void GenerateFlatChunk(Chunk *chunk) { + for (int x = 0; x < CHUNK_SIZE_X; x++) { + for (int z = 0; z < CHUNK_SIZE_Z; z++) { + for (int y = 0; y < CHUNK_SIZE_Y; y++) { + if (y < 3) { + chunk->blocks[x][y][z].type = BLOCK_STONE; + } else if (y < 7) { + chunk->blocks[x][y][z].type = BLOCK_DIRT; + } else if (y == 7) { + chunk->blocks[x][y][z].type = BLOCK_GRASS; + } else { + chunk->blocks[x][y][z].type = BLOCK_AIR; + } + } + } + } +} diff --git a/source/chunkRenderer.c b/source/chunkRenderer.c new file mode 100644 index 0000000..4839635 --- /dev/null +++ b/source/chunkRenderer.c @@ -0,0 +1,147 @@ +// chunkRenderer.c +// Rendering and meshing functions for voxelThing +// TODO: Memory is allocated dynamically in here but never freed. +// TODO: Chunk meshes need to be unloaded from the VRAM with UnloadMesh() when chunks are updated. +// For now new meshes are just added to the VRAM after each update... + +#include +#include "raylib.h" +#include "blockTypes.h" +#include "atlasDefinitions.h" +#include "chunkRenderer.h" + +#define TILE_SIZE 16 +#define ATLAS_SIZE 256 +#define ATLAS_TILES_PER_ROW (ATLAS_SIZE / TILE_SIZE) + +/// Returns the UV coordinate for a given tile index and corner index (0–3), +/// assuming tiles are arranged in a grid in the texture atlas. +Vector2 GetTileUV(int tile, int corner) { + int tileX = tile % ATLAS_TILES_PER_ROW; + int tileY = tile / ATLAS_TILES_PER_ROW; + + float u = (float)(tileX * TILE_SIZE) / ATLAS_SIZE; + float v = (float)(tileY * TILE_SIZE) / ATLAS_SIZE; + float duv = (float)TILE_SIZE / ATLAS_SIZE; + + switch (corner) { + case 0: return (Vector2){ u, v }; + case 1: return (Vector2){ u + duv, v }; + case 2: return (Vector2){ u + duv, v + duv }; + case 3: return (Vector2){ u, v + duv }; + default: return (Vector2){ 0.0f, 0.0f }; // Should not happen + } +} + +/// Generates a mesh for the given chunk by stitching together visible block faces. +Mesh GenerateChunkMesh(Chunk *chunk) { + const int maxFaces = CHUNK_SIZE_X * CHUNK_SIZE_Y * CHUNK_SIZE_Z * 6; + const int maxVerts = maxFaces * 4; // 4 verts per face + const int maxIndices = maxFaces * 6; // 2 triangles per face + + Vector3 *vertices = malloc(sizeof(Vector3) * maxVerts); + Vector3 *normals = malloc(sizeof(Vector3) * maxVerts); + Vector2 *texcoords = malloc(sizeof(Vector2) * maxVerts); + unsigned short *indices = malloc(sizeof(unsigned short) * maxIndices); + + int vertCount = 0; + int indexCount = 0; + + for (int x = 0; x < CHUNK_SIZE_X; x++) { + for (int y = 0; y < CHUNK_SIZE_Y; y++) { + for (int z = 0; z < CHUNK_SIZE_Z; z++) { + + Block block = chunk->blocks[x][y][z]; + if (block.type == 0) continue; // Skip air blocks + + Vector3 min = { x, y, z }; + Vector3 max = { x + 1.0f, y + 1.0f, z + 1.0f }; + + for (int dir = 0; dir < 6; dir++) { + if (!IsBlockFaceExposed(chunk, x, y, z, dir)) continue; + + Vector3 face[4]; + + switch (dir) { + case 0: // -X + face[0] = (Vector3){ min.x, max.y, min.z }; + face[1] = (Vector3){ min.x, max.y, max.z }; + face[2] = (Vector3){ min.x, min.y, max.z }; + face[3] = (Vector3){ min.x, min.y, min.z }; + break; + case 1: // +X + face[0] = (Vector3){ max.x, max.y, max.z }; + face[1] = (Vector3){ max.x, max.y, min.z }; + face[2] = (Vector3){ max.x, min.y, min.z }; + face[3] = (Vector3){ max.x, min.y, max.z }; + break; + case 2: // -Y + face[0] = (Vector3){ min.x, min.y, max.z }; + face[1] = (Vector3){ max.x, min.y, max.z }; + face[2] = (Vector3){ max.x, min.y, min.z }; + face[3] = (Vector3){ min.x, min.y, min.z }; + break; + case 3: // +Y + face[0] = (Vector3){ min.x, max.y, min.z }; + face[1] = (Vector3){ max.x, max.y, min.z }; + face[2] = (Vector3){ max.x, max.y, max.z }; + face[3] = (Vector3){ min.x, max.y, max.z }; + break; + case 4: // -Z + face[0] = (Vector3){ max.x, max.y, min.z }; + face[1] = (Vector3){ min.x, max.y, min.z }; + face[2] = (Vector3){ min.x, min.y, min.z }; + face[3] = (Vector3){ max.x, min.y, min.z }; + break; + case 5: // +Z + face[0] = (Vector3){ min.x, max.y, max.z }; + face[1] = (Vector3){ max.x, max.y, max.z }; + face[2] = (Vector3){ max.x, min.y, max.z }; + face[3] = (Vector3){ min.x, min.y, max.z }; + break; + } + + const BlockType *bt = GetBlockType(block.type); + int tileIndex = bt->faceTiles[dir]; + + Vector3 normal = {0}; + switch (dir) { + case 0: normal = (Vector3){ -1.0f, 0.0f, 0.0f }; break; + case 1: normal = (Vector3){ 1.0f, 0.0f, 0.0f }; break; + case 2: normal = (Vector3){ 0.0f, -1.0f, 0.0f }; break; + case 3: normal = (Vector3){ 0.0f, 1.0f, 0.0f }; break; + case 4: normal = (Vector3){ 0.0f, 0.0f, -1.0f }; break; + case 5: normal = (Vector3){ 0.0f, 0.0f, 1.0f }; break; + } + + for (int i = 0; i < 4; i++) { + vertices[vertCount] = face[i]; + texcoords[vertCount] = GetTileUV(tileIndex, i); + normals[vertCount] = normal; + vertCount++; + } + + indices[indexCount++] = vertCount - 4; + indices[indexCount++] = vertCount - 2; + indices[indexCount++] = vertCount - 3; + indices[indexCount++] = vertCount - 4; + indices[indexCount++] = vertCount - 1; + indices[indexCount++] = vertCount - 2; + } + } + } + } + + Mesh mesh = {0}; + mesh.vertexCount = vertCount; + mesh.triangleCount = indexCount / 3; + + mesh.vertices = (float *)vertices; + mesh.texcoords = (float *)texcoords; + mesh.normals = (float *)normals; + mesh.indices = indices; + + UploadMesh(&mesh, false); + return mesh; +} + diff --git a/source/chunkStructures.c b/source/chunkStructures.c new file mode 100644 index 0000000..3154393 --- /dev/null +++ b/source/chunkStructures.c @@ -0,0 +1,60 @@ +// chunkStructures.c +// This file contains functions for placing structures inside chunks, as well as the definitions for chunk datastructures... +// TODO: Should probably keep datastructures and chunk structures separate. + +#include "chunkStructures.h" +#include "blockTypes.h" +#include "raylib.h" +#include "rlgl.h" +#include +#include + +// Places a tree at the position specified by x, y, z +void PlaceTreeAt(Chunk *chunk, int x, int y, int z) { + // Trunk (6 blocks tall) + for (int i = 1; i <= 6; i++) { + if (y + i >= CHUNK_SIZE_Y) break; + chunk->blocks[x][y + i][z].type = BLOCK_LOG; + } + + // Canopy (2 layers of leaves) + for (int dy = 4; dy <= 6; dy++) { + for (int dx = -2; dx <= 2; dx++) { + for (int dz = -2; dz <= 2; dz++) { + int cx = x + dx; + int cy = y + dy; + int cz = z + dz; + + if (cx < 0 || cx >= CHUNK_SIZE_X || + cy < 0 || cy >= CHUNK_SIZE_Y || + cz < 0 || cz >= CHUNK_SIZE_Z) + continue; + + // Keep the center clear above trunk + if (dx == 0 && dz == 0 && dy == 4) continue; + + // Slightly sparser at corners + if (abs(dx) == 2 && abs(dz) == 2) continue; + + chunk->blocks[cx][cy][cz].type = BLOCK_LEAF; + } + } + } +} + +int IsBlockFaceExposed(Chunk *chunk, int x, int y, int z, int dir) { + int nx = x + faceOffsets[dir][0]; + int ny = y + faceOffsets[dir][1]; + int nz = z + faceOffsets[dir][2]; + + if (nx < 0 || ny < 0 || nz < 0 || + nx >= CHUNK_SIZE_X || ny >= CHUNK_SIZE_Y || nz >= CHUNK_SIZE_Z) { + return 1; // face is exposed at chunk boundary + } + + return chunk->blocks[nx][ny][nz].type == 0; +} + + + + diff --git a/source/playerController.c b/source/playerController.c new file mode 100644 index 0000000..4760ee8 --- /dev/null +++ b/source/playerController.c @@ -0,0 +1,182 @@ +// playerController.c +// Player controller for Voxelthing + +#include"raylib.h" +#include"raymath.h" +#include "rlgl.h" +#include"playerController.h" +#include"blockTypes.h" + +float yaw = 0; +float pitch = 0; + +// A basic free moving, 'noclip' style camera to get going with the most basic interactions. +// returns yaw because I'm confused and am trying to help +void UpdateFreeCamera(Camera3D *cam, float speed, float *yawOut, float *pitchOut) { + Vector2 mouseDelta = GetMouseDelta(); + + yaw += mouseDelta.x * -0.002f; + pitch += mouseDelta.y * -0.002f; + *yawOut = yaw; + *pitchOut = pitch; + + // Clamp pitch + float clampLimit = PI / 1.80f; + if (pitch > clampLimit) pitch = clampLimit; + if (pitch < -clampLimit) pitch = -clampLimit; + + // Compute forward vector from yaw/pitch + Vector3 forward = { + cosf(pitch) * sinf(yaw), + sinf(pitch), + cosf(pitch) * cosf(yaw) + }; + + Vector3 right = { + sinf(yaw - PI / 2.0f), + 0.0f, + cosf(yaw - PI / 2.0f) + }; + + // Movement input + Vector3 movement = {0}; + if (IsKeyDown(KEY_W)) movement = Vector3Add(movement, forward); + if (IsKeyDown(KEY_S)) movement = Vector3Subtract(movement, forward); + if (IsKeyDown(KEY_A)) movement = Vector3Subtract(movement, right); + if (IsKeyDown(KEY_D)) movement = Vector3Add(movement, right); + if (IsKeyDown(KEY_SPACE)) movement.y += 1.0f; + if (IsKeyDown(KEY_LEFT_SHIFT)) movement.y -= 1.0f; + + // Apply movement + if (Vector3Length(movement) > 0.0f) + movement = Vector3Scale(Vector3Normalize(movement), speed * GetFrameTime()); + cam->position = Vector3Add(cam->position, movement); + + // Update target so that the camera looks forward + cam->target = Vector3Add(cam->position, forward); + // return the value of cameraYaw... +} + +// An implementation of DDA (digital differential analyzer), steps through each voxel boundary along a ray cast from origin along direction to maxDistance +RaycastHit RaycastChunk(const Chunk *chunk, Vector3 origin, Vector3 direction, float maxDistance) { + RaycastHit result = {0}; // Initialize result: no hit, zeroed values + + direction = Vector3Normalize(direction); // Ensure direction is a unit vector + + // Nudge the origin slightly to avoid precision issues at block boundaries + origin = Vector3Add(origin, Vector3Scale(direction, 0.001f)); + + // Determine the next voxel boundary to cross along each axis + float nextX = (direction.x >= 0) ? ceilf(origin.x) : floorf(origin.x); + float nextY = (direction.y >= 0) ? ceilf(origin.y) : floorf(origin.y); + float nextZ = (direction.z >= 0) ? ceilf(origin.z) : floorf(origin.z); + + // Get integer voxel coordinates for current position + int x = (int)floorf(origin.x); + int y = (int)floorf(origin.y); + int z = (int)floorf(origin.z); + + // Determine step direction: +1 or -1 along each axis + int stepX = (direction.x >= 0) ? 1 : -1; + int stepY = (direction.y >= 0) ? 1 : -1; + int stepZ = (direction.z >= 0) ? 1 : -1; + + // How far to travel along the ray to cross a voxel in each axis + float tDeltaX = (direction.x == 0) ? INFINITY : fabsf(1.0f / direction.x); + float tDeltaY = (direction.y == 0) ? INFINITY : fabsf(1.0f / direction.y); + float tDeltaZ = (direction.z == 0) ? INFINITY : fabsf(1.0f / direction.z); + + // Distance from origin to the first voxel boundary (for each axis) + float tMaxX = (direction.x == 0) ? INFINITY : (nextX - origin.x) / direction.x; + float tMaxY = (direction.y == 0) ? INFINITY : (nextY - origin.y) / direction.y; + float tMaxZ = (direction.z == 0) ? INFINITY : (nextZ - origin.z) / direction.z; + + float t = 0.0f; // Total traveled distance along the ray + Vector3 lastNormal = {0}; // Which face was entered (used for highlighting/interactions) + + // Walk the ray through the voxel grid until we exceed maxDistance + while (t < maxDistance) { + // Check if the current voxel is inside the chunk bounds + if (x >= 0 && y >= 0 && z >= 0 && + x < CHUNK_SIZE_X && y < CHUNK_SIZE_Y && z < CHUNK_SIZE_Z) { + + int blockID = chunk->blocks[x][y][z].type; + + // If it's not air, we hit something! + if (blockID != BLOCK_AIR) { + result.hit = true; + result.blockID = blockID; + result.position = (Vector3){x, y, z}; + result.normal = lastNormal; + result.t = t; + return result; + } + } + + // Move to the next voxel along the smallest tMax (i.e., the closest boundary) + if (tMaxX < tMaxY && tMaxX < tMaxZ) { + x += stepX; + t = tMaxX; + tMaxX += tDeltaX; + lastNormal = (Vector3){-stepX, 0, 0}; // Normal points opposite the ray step + } else if (tMaxY < tMaxZ) { + y += stepY; + t = tMaxY; + tMaxY += tDeltaY; + lastNormal = (Vector3){0, -stepY, 0}; + } else { + z += stepZ; + t = tMaxZ; + tMaxZ += tDeltaZ; + lastNormal = (Vector3){0, 0, -stepZ}; + } + } + + // If no block was hit, return default (no hit) + return result; +} + +void DrawFaceHighlight(Vector3 blockPos, Vector3 normal) { + Vector3 center = Vector3Add(blockPos, (Vector3){0.5f, 0.5f, 0.5f}); + Vector3 faceCenter = Vector3Add(center, Vector3Scale(normal, 0.51f)); + + Vector3 u = {0}, v = {0}; + + if (normal.x != 0) { + u = (Vector3){0, 0, 0.5f}; + v = (Vector3){0, 0.5f, 0}; + } else if (normal.y != 0) { + u = (Vector3){0.5f, 0, 0}; + v = (Vector3){0, 0, 0.5f}; + } else { + u = (Vector3){0.5f, 0, 0}; + v = (Vector3){0, 0.5f, 0}; + } + + Vector3 corners[4] = { + Vector3Add(Vector3Add(faceCenter, u), v), + Vector3Add(Vector3Subtract(faceCenter, u), v), + Vector3Add(Vector3Subtract(faceCenter, u), Vector3Negate(v)), + Vector3Add(Vector3Add(faceCenter, u), Vector3Negate(v)) + }; + + // Flip winding for certain normals so the face always faces outward + bool flip = false; + if (normal.x == 1 || normal.y == 1 || normal.z == -1) flip = true; + + rlBegin(RL_QUADS); + rlColor4ub(0, 255, 0, 100); + + if (flip) { + for (int i = 3; i >= 0; i--) { + rlVertex3f(corners[i].x, corners[i].y, corners[i].z); + } + } else { + for (int i = 0; i < 4; i++) { + rlVertex3f(corners[i].x, corners[i].y, corners[i].z); + } + } + + rlEnd(); +} + diff --git a/source/voxelThing.c b/source/voxelThing.c new file mode 100644 index 0000000..9af4648 --- /dev/null +++ b/source/voxelThing.c @@ -0,0 +1,156 @@ +// voxelThing.c +// A voxel chunk rendering doodad written in C with Raylib + +// External libraries +#include "raylib.h" +#include "raymath.h" +#include "rlgl.h" +#include "stdio.h" + +// Project files +#include "chunkStructures.h" +#include "chunkRenderer.h" +#include "blockTypes.h" +#include "playerController.h" +#include "chunkGenerator.h" + +float cameraYaw = 0; +float cameraPitch = 0; + +const char* GetCompassDirection(float yaw) { + yaw = fmodf(yaw * RAD2DEG + 360.0f, 360.0f); + if (yaw < 22.5f || yaw >= 337.5f) return "South (+Z)"; + if (yaw < 67.5f) return "Southwest (+X+Z)"; + if (yaw < 112.5f) return "West (+X)"; + if (yaw < 157.5f) return "Northwest (+X-Z)"; + if (yaw < 202.5f) return "North (-Z)"; + if (yaw < 247.5f) return "Northeast (-X-Z)"; + if (yaw < 292.5f) return "East (-X)"; + return "Southeast (-X+Z)"; +} + +int main(void) { + // --- Screen setup --- + const int screenWidth = 800; + const int screenHeight = 600; + InitWindow(screenWidth, screenHeight, "VoxelThing"); + DisableCursor(); // Lock mouse to window for FPS-style camera + + // --- World generation --- + Chunk chunk; + GenerateFlatChunk(&chunk); + PlaceTreeAt(&chunk, 8, 7, 8); + Mesh chunkMesh = GenerateChunkMesh(&chunk); + + // --- Load textures and materials --- + Texture2D atlas = LoadTexture("assets/TextureAtlas.png"); + Material mat = LoadMaterialDefault(); + mat.maps[MATERIAL_MAP_DIFFUSE].texture = atlas; + + // --- Initialize camera --- + Camera3D camera = { 0 }; + camera.position = (Vector3){ 10.0f, 10.0f, 10.0f }; // Initial camera position + camera.target = (Vector3){ 0.0f, 0.0f, 0.0f }; // Looking toward origin + camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Y-up world + camera.fovy = 45.0f; // Field of view + camera.projection = CAMERA_PERSPECTIVE; // Use perspective projection + + SetTargetFPS(60); + + // --- Main game loop --- + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(RAYWHITE); + + // --- Update camera and direction --- + + UpdateFreeCamera(&camera, 10.0f, &cameraYaw, &cameraPitch); // Move camera with user input + + + // --- Raycasting from screen center --- + Vector2 screenCenter = { screenWidth / 2.0f, screenHeight / 2.0f }; + // This is where we grab the ray... + // Ray ray = GetMouseRay(screenCenter, camera); + Vector3 camDir = { + cosf(cameraPitch) * sinf(cameraYaw), + sinf(cameraPitch), + cosf(cameraPitch) * cosf(cameraYaw) + }; + + Ray ray = { + .position = camera.position, + .direction = Vector3Normalize(camDir) + }; + RaycastHit hit = RaycastChunk(&chunk, ray.position, ray.direction, 10.0f); + + // --- Begin 3D rendering --- + BeginMode3D(camera); + + DrawMesh(chunkMesh, mat, MatrixIdentity()); + + if (hit.hit) { + // Draw a wireframe cube where the ray hit + DrawCubeWires(Vector3Add(hit.position, (Vector3){0.5f, 0.5f, 0.5f}), 1.02f, 1.02f, 1.02f, RED); + DrawFaceHighlight(hit.position, hit.normal); // Highlight the specific face hit + } + // Draw debug ray. + camDir = Vector3Normalize(Vector3Subtract(camera.target, camera.position)); + Vector3 rayEnd = Vector3Add(camera.position, Vector3Scale(camDir, 10.0f)); + DrawLine3D(camera.position, rayEnd, PURPLE); + + Vector3 hitPoint = Vector3Add(ray.position, Vector3Scale(ray.direction, hit.t)); + DrawCubeWires(hitPoint, 0.05f, 0.05f, 0.05f, RED); + + EndMode3D(); + + Vector3 testPoint = Vector3Add(camera.position, Vector3Scale(camDir, 2.0f)); + Vector2 projected = GetWorldToScreen(testPoint, camera); + DrawCircleV(projected, 4, RED); + + // --- Draw crosshair in screen center --- + DrawLine(screenWidth/2 - 5, screenHeight/2, screenWidth/2 + 5, screenHeight/2, DARKGRAY); + DrawLine(screenWidth/2, screenHeight/2 - 5, screenWidth/2, screenHeight/2 + 5, DARKGRAY); + + // -- Draw debug info --- + DrawText(TextFormat("Facing: %s", GetCompassDirection(cameraYaw)), 10, 10, 20, DARKGRAY); + DrawText(TextFormat("Yaw: %.1f° Pitch: %.1f°", cameraYaw * RAD2DEG, cameraPitch * RAD2DEG), 10, 30, 20, GRAY); + + EndDrawing(); + + // TODO: Shoudn't all this block handling be handled in playerController.c? + + // --- Handle block removal (left click) --- + if (hit.hit && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + chunk.blocks[(int)hit.position.x][(int)hit.position.y][(int)hit.position.z].type = BLOCK_AIR; + chunkMesh = GenerateChunkMesh(&chunk); // Rebuild mesh after change + } + + // --- Handle block placement (right click) --- + if (hit.hit && IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { + int px = (int)(hit.position.x + hit.normal.x); + int py = (int)(hit.position.y + hit.normal.y); + int pz = (int)(hit.position.z + hit.normal.z); + + // Bounds check + if (px >= 0 && px < CHUNK_SIZE_X && + py >= 0 && py < CHUNK_SIZE_Y && + pz >= 0 && pz < CHUNK_SIZE_Z) { + + chunk.blocks[px][py][pz].type = BLOCK_SAND; + chunkMesh = GenerateChunkMesh(&chunk); + + printf("Hit at (%f %f %f), normal (%f %f %f), placing at (%d %d %d)\n", + hit.position.x, hit.position.y, hit.position.z, + hit.normal.x, hit.normal.y, hit.normal.z, + px, py, pz); + } + } + } + + // --- Cleanup --- + UnloadTexture(atlas); + UnloadMesh(chunkMesh); + UnloadMaterial(mat); + CloseWindow(); + return 0; +}