diff --git a/assets/heightmap_large.bin b/assets/heightmap_large.bin new file mode 100644 index 0000000..38b45a1 Binary files /dev/null and b/assets/heightmap_large.bin differ diff --git a/assets/heightmap_small.bin b/assets/heightmap_small.bin new file mode 100644 index 0000000..3e6315a Binary files /dev/null and b/assets/heightmap_small.bin differ diff --git a/client/config.hpp b/client/config.hpp new file mode 100644 index 0000000..cab8d74 --- /dev/null +++ b/client/config.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +struct GameConfig { + // Terrain configuration + std::string heightmapFile = "../assets/heightmap.bin"; + float unitsPerSample = 1.0f; // Meters per heightmap sample + float heightScale = 1.0f; // Height multiplier + + // Window configuration + int windowWidth = 1280; + int windowHeight = 720; + std::string windowTitle = "Game"; + int targetFPS = 60; + + // Movement + float moveSpeed = 15.0f; + + // Network + float heartbeatInterval = 5.0f; + + // Debug options + bool showDebugInfo = false; + + // Load configuration from command line args or config file + static GameConfig fromArgs(int argc, char* argv[]) { + GameConfig config; + + // Parse command line arguments + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + + if (arg == "--heightmap" && i + 1 < argc) { + config.heightmapFile = argv[++i]; + } else if (arg == "--scale" && i + 1 < argc) { + config.unitsPerSample = std::stof(argv[++i]); + } else if (arg == "--height-scale" && i + 1 < argc) { + config.heightScale = std::stof(argv[++i]); + } else if (arg == "--debug") { + config.showDebugInfo = true; + } else if (arg == "--help") { + printHelp(); + exit(0); + } + } + + return config; + } + +private: + static void printHelp() { + std::cout << "Game Options:\n"; + std::cout << " --heightmap Load specific heightmap file\n"; + std::cout << " --scale Units per heightmap sample (default: 1.0)\n"; + std::cout << " --height-scale Height multiplier (default: 1.0)\n"; + std::cout << " --debug Show debug information\n"; + std::cout << " --help Show this help\n"; + std::cout << "\nExamples:\n"; + std::cout << " ./game --heightmap ../assets/heightmap_small.bin\n"; + std::cout << " ./game --heightmap ../assets/heightmap_large.bin --scale 2.0\n"; + } +}; diff --git a/client/terrain/Heightmap.hpp b/client/terrain/Heightmap.hpp new file mode 100644 index 0000000..1b451ce --- /dev/null +++ b/client/terrain/Heightmap.hpp @@ -0,0 +1,206 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Heightmap { +public: + struct Config { + float unitsPerSample = 1.0f; // How many world units (meters) per heightmap sample + float heightScale = 1.0f; // Scale factor for height values + }; + +private: + std::vector data; + int samplesPerSide{0}; // Number of samples along one edge + float worldWidth{0.0f}; // Total world width in units + float worldHeight{0.0f}; // Total world height in units + Config config; + +public: + Heightmap() : config{} {} + Heightmap(const Config& cfg) : config(cfg) {} + + // Load heightmap from binary file + bool load(const std::string& filename) { + std::ifstream file(filename, std::ios::binary); + if (!file) return false; + + int32_t fileSize; + file.read(reinterpret_cast(&fileSize), sizeof(fileSize)); + samplesPerSide = fileSize; + + data.resize(samplesPerSide * samplesPerSide); + file.read(reinterpret_cast(data.data()), data.size() * sizeof(float)); + + // Calculate world dimensions based on samples and units per sample + worldWidth = worldHeight = (samplesPerSide - 1) * config.unitsPerSample; + + return true; + } + + // Get interpolated height at world position + float getHeightAtPosition(float worldX, float worldZ) const { + // Convert world coordinates to sample coordinates + float sampleX = (worldX + worldWidth * 0.5f) / config.unitsPerSample; + float sampleZ = (worldZ + worldHeight * 0.5f) / config.unitsPerSample; + + // Clamp to valid range + sampleX = std::max(0.0f, std::min(sampleX, float(samplesPerSide - 1))); + sampleZ = std::max(0.0f, std::min(sampleZ, float(samplesPerSide - 1))); + + // Get integer sample indices + int x0 = static_cast(sampleX); + int z0 = static_cast(sampleZ); + int x1 = std::min(x0 + 1, samplesPerSide - 1); + int z1 = std::min(z0 + 1, samplesPerSide - 1); + + // Get fractional parts for interpolation + float fx = sampleX - x0; + float fz = sampleZ - z0; + + // Get heights at four corners + float h00 = data[z0 * samplesPerSide + x0] * config.heightScale; + float h10 = data[z0 * samplesPerSide + x1] * config.heightScale; + float h01 = data[z1 * samplesPerSide + x0] * config.heightScale; + float h11 = data[z1 * samplesPerSide + x1] * config.heightScale; + + // Bilinear interpolation + float h0 = h00 * (1.0f - fx) + h10 * fx; + float h1 = h01 * (1.0f - fx) + h11 * fx; + return h0 * (1.0f - fz) + h1 * fz; + } + + // Generate mesh from heightmap + Mesh generateMesh() const { + auto mesh = Mesh{}; + int vertexCount = samplesPerSide * samplesPerSide; + int triangleCount = (samplesPerSide - 1) * (samplesPerSide - 1) * 2; + + mesh.vertexCount = vertexCount; + mesh.triangleCount = triangleCount; + mesh.vertices = (float*)MemAlloc(vertexCount * 3 * sizeof(float)); + mesh.texcoords = (float*)MemAlloc(vertexCount * 2 * sizeof(float)); + mesh.normals = (float*)MemAlloc(vertexCount * 3 * sizeof(float)); + mesh.indices = (unsigned short*)MemAlloc(triangleCount * 3 * sizeof(unsigned short)); + + // Generate vertices + for (int z = 0; z < samplesPerSide; z++) { + for (int x = 0; x < samplesPerSide; x++) { + int idx = z * samplesPerSide + x; + + // World position in units (meters) + // Center the terrain around origin + float worldX = (x * config.unitsPerSample) - worldWidth * 0.5f; + float worldZ = (z * config.unitsPerSample) - worldHeight * 0.5f; + float worldY = data[idx] * config.heightScale; + + mesh.vertices[idx * 3] = worldX; + mesh.vertices[idx * 3 + 1] = worldY; + mesh.vertices[idx * 3 + 2] = worldZ; + + // Texture coordinates: 1 unit = 1 texture tile + // Map from world coordinates to texture coordinates + float texU = worldX + worldWidth * 0.5f; + float texV = worldZ + worldHeight * 0.5f; + + mesh.texcoords[idx * 2] = texU; + mesh.texcoords[idx * 2 + 1] = texV; + } + } + + // Generate indices + int triIdx = 0; + for (int z = 0; z < samplesPerSide - 1; z++) { + for (int x = 0; x < samplesPerSide - 1; x++) { + int topLeft = z * samplesPerSide + x; + int topRight = topLeft + 1; + int bottomLeft = (z + 1) * samplesPerSide + x; + int bottomRight = bottomLeft + 1; + + mesh.indices[triIdx++] = topLeft; + mesh.indices[triIdx++] = bottomLeft; + mesh.indices[triIdx++] = topRight; + + mesh.indices[triIdx++] = topRight; + mesh.indices[triIdx++] = bottomLeft; + mesh.indices[triIdx++] = bottomRight; + } + } + + // Calculate proper normals + calculateNormals(mesh); + + UploadMesh(&mesh, false); + return mesh; + } + + // Getters + float getWorldWidth() const { return worldWidth; } + float getWorldHeight() const { return worldHeight; } + Vector3 getWorldBounds() const { + return {worldWidth, getMaxHeight() * config.heightScale, worldHeight}; + } + Vector3 getWorldCenter() const { return {0, 0, 0}; } + int getSamplesPerSide() const { return samplesPerSide; } + +private: + void calculateNormals(Mesh& mesh) const { + // Initialize normals to zero + for (int i = 0; i < mesh.vertexCount * 3; i++) { + mesh.normals[i] = 0.0f; + } + + // Calculate face normals and add to vertex normals + for (int i = 0; i < mesh.triangleCount; i++) { + unsigned short i1 = mesh.indices[i * 3]; + unsigned short i2 = mesh.indices[i * 3 + 1]; + unsigned short i3 = mesh.indices[i * 3 + 2]; + + Vector3 v1 = {mesh.vertices[i1 * 3], mesh.vertices[i1 * 3 + 1], mesh.vertices[i1 * 3 + 2]}; + Vector3 v2 = {mesh.vertices[i2 * 3], mesh.vertices[i2 * 3 + 1], mesh.vertices[i2 * 3 + 2]}; + Vector3 v3 = {mesh.vertices[i3 * 3], mesh.vertices[i3 * 3 + 1], mesh.vertices[i3 * 3 + 2]}; + + Vector3 edge1 = Vector3Subtract(v2, v1); + Vector3 edge2 = Vector3Subtract(v3, v1); + Vector3 normal = Vector3Normalize(Vector3CrossProduct(edge1, edge2)); + + // Add face normal to each vertex + mesh.normals[i1 * 3] += normal.x; + mesh.normals[i1 * 3 + 1] += normal.y; + mesh.normals[i1 * 3 + 2] += normal.z; + + mesh.normals[i2 * 3] += normal.x; + mesh.normals[i2 * 3 + 1] += normal.y; + mesh.normals[i2 * 3 + 2] += normal.z; + + mesh.normals[i3 * 3] += normal.x; + mesh.normals[i3 * 3 + 1] += normal.y; + mesh.normals[i3 * 3 + 2] += normal.z; + } + + // Normalize vertex normals + for (int i = 0; i < mesh.vertexCount; i++) { + Vector3 normal = { + mesh.normals[i * 3], + mesh.normals[i * 3 + 1], + mesh.normals[i * 3 + 2] + }; + normal = Vector3Normalize(normal); + mesh.normals[i * 3] = normal.x; + mesh.normals[i * 3 + 1] = normal.y; + mesh.normals[i * 3 + 2] = normal.z; + } + } + + float getMaxHeight() const { + float maxHeight = 0.0f; + for (float h : data) { + maxHeight = std::max(maxHeight, h); + } + return maxHeight; + } +}; diff --git a/server/net/world.go b/server/net/world.go new file mode 100644 index 0000000..31b7950 --- /dev/null +++ b/server/net/world.go @@ -0,0 +1,89 @@ +package net + +import "math" + +// WorldConfig holds world configuration based on heightmap +type WorldConfig struct { + SamplesPerSide int // Number of heightmap samples per side + UnitsPerSample float32 // World units (meters) per sample + WorldWidth float32 // Total world width in units + WorldHeight float32 // Total world height (depth) in units + MinBounds Vec3 // Minimum world bounds + MaxBounds Vec3 // Maximum world bounds + MaxTerrainHeight float32 // Maximum terrain height +} + +// NewWorldConfig creates world configuration from heightmap +func NewWorldConfig(heightmap [][]float32, unitsPerSample float32) *WorldConfig { + samplesPerSide := len(heightmap) + worldSize := float32(samplesPerSide-1) * unitsPerSample + halfSize := worldSize * 0.5 + + // Find max terrain height + maxHeight := float32(0) + for y := range heightmap { + for x := range heightmap[y] { + if heightmap[y][x] > maxHeight { + maxHeight = heightmap[y][x] + } + } + } + + return &WorldConfig{ + SamplesPerSide: samplesPerSide, + UnitsPerSample: unitsPerSample, + WorldWidth: worldSize, + WorldHeight: worldSize, + MinBounds: Vec3{X: -halfSize, Y: 0, Z: -halfSize}, + MaxBounds: Vec3{X: halfSize, Y: maxHeight + 10, Z: halfSize}, + MaxTerrainHeight: maxHeight, + } +} + +// ClampPosition clamps a position to world bounds +func (w *WorldConfig) ClampPosition(pos Vec3) Vec3 { + return Vec3{ + X: float32(math.Max(float64(w.MinBounds.X), math.Min(float64(w.MaxBounds.X), float64(pos.X)))), + Y: float32(math.Max(float64(w.MinBounds.Y), math.Min(float64(w.MaxBounds.Y), float64(pos.Y)))), + Z: float32(math.Max(float64(w.MinBounds.Z), math.Min(float64(w.MaxBounds.Z), float64(pos.Z)))), + } +} + +// IsInBounds checks if a position is within world bounds +func (w *WorldConfig) IsInBounds(pos Vec3) bool { + return pos.X >= w.MinBounds.X && pos.X <= w.MaxBounds.X && + pos.Y >= w.MinBounds.Y && pos.Y <= w.MaxBounds.Y && + pos.Z >= w.MinBounds.Z && pos.Z <= w.MaxBounds.Z +} + +// GetHeightAt gets interpolated height at world position +func (w *WorldConfig) GetHeightAt(heightmap [][]float32, worldX, worldZ float32) float32 { + // Convert world coordinates to sample coordinates + sampleX := (worldX + w.WorldWidth*0.5) / w.UnitsPerSample + sampleZ := (worldZ + w.WorldHeight*0.5) / w.UnitsPerSample + + // Clamp to valid range + sampleX = float32(math.Max(0, math.Min(float64(w.SamplesPerSide-1), float64(sampleX)))) + sampleZ = float32(math.Max(0, math.Min(float64(w.SamplesPerSide-1), float64(sampleZ)))) + + // Get integer sample indices + x0 := int(math.Floor(float64(sampleX))) + z0 := int(math.Floor(float64(sampleZ))) + x1 := int(math.Min(float64(x0+1), float64(w.SamplesPerSide-1))) + z1 := int(math.Min(float64(z0+1), float64(w.SamplesPerSide-1))) + + // Get fractional parts for interpolation + fx := sampleX - float32(x0) + fz := sampleZ - float32(z0) + + // Get heights at four corners + h00 := heightmap[z0][x0] + h10 := heightmap[z0][x1] + h01 := heightmap[z1][x0] + h11 := heightmap[z1][x1] + + // Bilinear interpolation + h0 := h00*(1-fx) + h10*fx + h1 := h01*(1-fx) + h11*fx + return h0*(1-fz) + h1*fz +} \ No newline at end of file diff --git a/tools/generate_heightmap b/tools/generate_heightmap new file mode 100755 index 0000000..3efdc7a Binary files /dev/null and b/tools/generate_heightmap differ diff --git a/tools/generate_heightmap.cpp b/tools/generate_heightmap.cpp new file mode 100644 index 0000000..0ac77aa --- /dev/null +++ b/tools/generate_heightmap.cpp @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include +#include + +// Simple heightmap generator for testing different world sizes +void generateHeightmap(const std::string& filename, int size, float amplitude = 10.0f) { + std::vector data(size * size); + + // Generate a simple heightmap with some hills + for (int z = 0; z < size; z++) { + for (int x = 0; x < size; x++) { + float fx = (float)x / (size - 1) * 2.0f - 1.0f; + float fz = (float)z / (size - 1) * 2.0f - 1.0f; + + // Create some hills using sine waves + float height = 0; + height += sin(fx * 3.14159f * 2.0f) * cos(fz * 3.14159f * 2.0f) * amplitude * 0.5f; + height += sin(fx * 3.14159f * 4.0f) * sin(fz * 3.14159f * 4.0f) * amplitude * 0.25f; + + // Add a central mountain + float dist = sqrt(fx * fx + fz * fz); + height += std::max(0.0f, (1.0f - dist) * amplitude); + + data[z * size + x] = height; + } + } + + // Write to binary file + std::ofstream file(filename, std::ios::binary); + if (!file) { + std::cerr << "Failed to open file: " << filename << "\n"; + return; + } + + int32_t fileSize = size; + file.write(reinterpret_cast(&fileSize), sizeof(fileSize)); + file.write(reinterpret_cast(data.data()), data.size() * sizeof(float)); + + std::cout << "Generated heightmap: " << filename << " (" << size << "x" << size << ")\n"; + std::cout << "World size will be: " << (size-1) << "x" << (size-1) << " units\n"; +} + +int main(int argc, char* argv[]) { + if (argc < 3) { + std::cout << "Usage: " << argv[0] << " [amplitude]\n"; + std::cout << "Example: " << argv[0] << " 256 heightmap_256.bin 20.0\n"; + std::cout << "\nCommon sizes:\n"; + std::cout << " 64x64 -> 63x63 unit world (small)\n"; + std::cout << " 128x128 -> 127x127 unit world (medium)\n"; + std::cout << " 256x256 -> 255x255 unit world (large)\n"; + std::cout << " 512x512 -> 511x511 unit world (huge)\n"; + return 1; + } + + int size = std::stoi(argv[1]); + std::string filename = argv[2]; + float amplitude = (argc > 3) ? std::stof(argv[3]) : 10.0f; + + if (size < 2) { + std::cerr << "Size must be at least 2\n"; + return 1; + } + + generateHeightmap(filename, size, amplitude); + return 0; +} \ No newline at end of file