From 5a0d81134ed88c2eebe1c64ad7a037b98892819c Mon Sep 17 00:00:00 2001 From: Yggdrasil75 Date: Wed, 5 Nov 2025 11:56:56 -0500 Subject: [PATCH] init --- c.cpp | 937 ++++++++++++++++++++++++++++++++++++++ d.cpp | 659 +++++++++++++++++++++++++++ e.cpp | 854 ++++++++++++++++++++++++++++++++++ f.cpp | 363 +++++++++++++++ g.cpp | 17 + gradient.bmp | Bin 0 -> 786486 bytes main.cpp | 136 ++++++ pointcloud_renderer | Bin 0 -> 48416 bytes sim.cpp | 479 +++++++++++++++++++ util/Vec2.hpp | 286 ++++++++++++ util/bmpwriter.hpp | 167 +++++++ util/grid2.hpp | 231 ++++++++++ util/ray.cpp | 27 ++ util/ray2.hpp | 59 +++ util/ray3.hpp | 73 +++ util/ray4.hpp | 52 +++ util/timing_decorator.cpp | 115 +++++ util/timing_decorator.hpp | 100 ++++ util/vec.cpp | 19 + util/vec3.hpp | 322 +++++++++++++ util/vec4.hpp | 384 ++++++++++++++++ util/voxelgrid.hpp | 217 +++++++++ 22 files changed, 5497 insertions(+) create mode 100644 c.cpp create mode 100644 d.cpp create mode 100644 e.cpp create mode 100644 f.cpp create mode 100644 g.cpp create mode 100644 gradient.bmp create mode 100644 main.cpp create mode 100644 pointcloud_renderer create mode 100644 sim.cpp create mode 100644 util/Vec2.hpp create mode 100644 util/bmpwriter.hpp create mode 100644 util/grid2.hpp create mode 100644 util/ray.cpp create mode 100644 util/ray2.hpp create mode 100644 util/ray3.hpp create mode 100644 util/ray4.hpp create mode 100644 util/timing_decorator.cpp create mode 100644 util/timing_decorator.hpp create mode 100644 util/vec.cpp create mode 100644 util/vec3.hpp create mode 100644 util/vec4.hpp create mode 100644 util/voxelgrid.hpp diff --git a/c.cpp b/c.cpp new file mode 100644 index 0000000..26c9049 --- /dev/null +++ b/c.cpp @@ -0,0 +1,937 @@ +#include +#include +#include +#include +#include +#include +#include +#include "timing_decorator.hpp" +#include +#include +#include + +const float EPSILON = 0.00000000001; + +//classes and structs + +struct Bool3 { + bool x, y, z; +}; + +class Vec3 { +public: + double x, y, z; + + Vec3(double x = 0, double y = 0, double z = 0) : x(x), y(y), z(z) {} + + //Vec3(const VoxelIndex& idx) : x(static_cast(idx.x)), y(static_cast(idx.y)), z(static_cast(idx.z)) {} + + inline double norm() const { + return std::sqrt(x*x + y*y + z*z); + } + + inline Vec3 normalize() const { + double n = norm(); + return Vec3(x/n, y/n, z/n); + } + + inline Vec3 cross(const Vec3& other) const { + return Vec3( + y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x + ); + } + + inline double dot(const Vec3& other) const { + return x * other.x + y * other.y + z * other.z; + } + + inline Vec3 operator+(float scalar) const { + return Vec3(x + scalar, y + scalar, z + scalar); + } + + inline Vec3 operator+(const Vec3& other) const { + return Vec3(x + other.x, y + other.y, z + other.z); + } + + Vec3 operator+(const Bool3& other) const { + return { + x + (other.x ? 1.0f : 0.0f), + y + (other.y ? 1.0f : 0.0f), + z + (other.z ? 1.0f : 0.0f) + }; + } + + inline Vec3 operator-(const Vec3& other) const { + return Vec3(x - other.x, y - other.y, z - other.z); + } + + inline Vec3 operator*(float scalar) const { + return Vec3(x * scalar, y * scalar, z * scalar); + } + + inline Vec3 operator*(const Vec3& scalar) const { + return Vec3(x * scalar.x, y * scalar.y, z * scalar.z); + } + + inline friend Vec3 operator*(float scalar, const Vec3& vec) { + return Vec3(scalar * vec.x, scalar * vec.y, scalar * vec.z); + } + + inline Vec3 operator/(float scalar) const { + return Vec3(x / scalar, y / scalar, z / scalar); + } + + inline Vec3 operator/(const Vec3& scalar) const { + return Vec3(x / scalar.x, y / scalar.y, z / scalar.z); + } + + inline friend Vec3 operator/(float scalar, const Vec3& vec) { + return Vec3(vec.x / scalar, vec.y / scalar, vec.z / scalar); + } + + bool operator==(const Vec3& other) const { + return x == other.x && y == other.y && z == other.z; + } + + bool operator<(const Vec3& other) const { + if (x != other.x) return x < other.x; + if (y != other.y) return y < other.y; + return z < other.z; + } + + const float& operator[](int index) const { + if (index == 0) return x; + if (index == 1) return y; + if (index == 2) return z; + throw std::out_of_range("Index out of range"); + } + + double& operator[](int index) { + if (index == 0) return x; + if (index == 1) return y; + if (index == 2) return z; + throw std::out_of_range("Index out of range"); + } + + struct Hash { + size_t operator()(const Vec3& v) const { + size_t h1 = std::hash()(std::round(v.x * 1000.0)); + size_t h2 = std::hash()(std::round(v.y * 1000.0)); + size_t h3 = std::hash()(std::round(v.z * 1000.0)); + return h1 ^ (h2 << 1) ^ (h3 << 2); + } + }; + + std::string toString() const { + return "Vec3(" + std::to_string(x) + ", " + + std::to_string(y) + ", " + + std::to_string(z) + ")"; + } + + Vec3& safe_inverse_dir(float epsilon = 1e-6f) { + x = (std::abs(x) > epsilon) ? x : std::copysign(epsilon, -x); + y = (std::abs(y) > epsilon) ? y : std::copysign(epsilon, -y); + z = (std::abs(z) > epsilon) ? z : std::copysign(epsilon, -z); + return *this; + } + + Vec3 sign() const { + return Vec3( + (x > 0) ? 1 : ((x < 0) ? -1 : 0), + (y > 0) ? 1 : ((y < 0) ? -1 : 0), + (z > 0) ? 1 : ((z < 0) ? -1 : 0) + ); + } + + Vec3 abs() { + return Vec3(std::abs(x), std::abs(y), std::abs(z)); + } +}; + +class Vec4 { +public: + double x, y, z, w; + + Vec4(double x = 0, double y = 0, double z = 0, double w = 0) : x(x), y(y), z(z), w(w) {} + + inline double norm() const { + return std::sqrt(x*x + y*y + z*z + w*w); + } + + inline Vec4 normalize() const { + double n = norm(); + return Vec4(x/n, y/n, z/n, w/n); + } + + inline std::array wedge(const Vec4& other) const { + return { + Vec4(0, x*other.y - y*other.x, 0, 0), // xy-plane + Vec4(0, 0, x*other.z - z*other.x, 0), // xz-plane + Vec4(0, 0, 0, x*other.w - w*other.x), // xw-plane + Vec4(0, 0, y*other.z - z*other.y, 0), // yz-plane + Vec4(0, 0, 0, y*other.w - w*other.y), // yw-plane + Vec4(0, 0, 0, z*other.w - w*other.z) // zw-plane + }; + } + inline double dot(const Vec4& other) const { + return x * other.x + y * other.y + z * other.z + w * other.w; + } + + inline Vec4 operator+(const Vec4& other) const { + return Vec4(x + other.x, y + other.y, z + other.z, w + other.w); + } + + inline Vec4 operator-(const Vec4& other) const { + return Vec4(x - other.x, y - other.y, z - other.z, w - other.w); + } + + inline Vec4 operator*(double scalar) const { + return Vec4(x * scalar, y * scalar, z * scalar, w * scalar); + } + + inline Vec4 operator/(double scalar) const { + return Vec4(x / scalar, y / scalar, z / scalar, w / scalar); + } + + bool operator==(const Vec4& other) const { + return x == other.x && y == other.y && z == other.z && w == other.w; + } + + bool operator<(const Vec4& other) const { + if (x != other.x) return x < other.x; + if (y != other.y) return y < other.y; + if (z != other.z) return z < other.z; + return w < other.w; + } + + // Additional useful methods for 4D vectors + inline Vec4 homogenize() const { + if (w == 0) return *this; + return Vec4(x/w, y/w, z/w, 1.0); + } + + inline Vec3 xyz() const { + return Vec3(x, y, z); + } + + struct Hash { + size_t operator()(const Vec4& v) const { + size_t h1 = std::hash()(std::round(v.x * 1000.0)); + size_t h2 = std::hash()(std::round(v.y * 1000.0)); + size_t h3 = std::hash()(std::round(v.z * 1000.0)); + size_t h4 = std::hash()(std::round(v.w * 1000.0)); + return h1 ^ (h2 << 1) ^ (h3 << 2) ^ (h4 << 3); + } + }; +}; + +struct VoxelIndex { + int x, y, z; + + // Constructor + VoxelIndex(int x = 0, int y = 0, int z = 0) : x(x), y(y), z(z) {} + + // Array-like access + int& operator[](size_t index) { + switch(index) { + case 0: return x; + case 1: return y; + case 2: return z; + default: throw std::out_of_range("Index out of range"); + } + } + + const int& operator[](size_t index) const { + switch(index) { + case 0: return x; + case 1: return y; + case 2: return z; + default: throw std::out_of_range("Index out of range"); + } + } + + inline VoxelIndex operator+(const Vec3& other) const { + return VoxelIndex(std::floor(x + other.x), std::floor(y + other.y), std::floor(z + other.z)); + } + + inline VoxelIndex operator-(const Vec3& other) const { + return VoxelIndex(std::floor(x - other.x), std::floor(y - other.y), std::floor(z - other.z)); + } + + inline VoxelIndex operator+(float scalar) const { + return VoxelIndex(std::floor(x + scalar), std::floor(y + scalar), std::floor(z + scalar)); + } + + VoxelIndex operator+(const Bool3& other) const { + return { + x + (other.x ? 1 : 0), + y + (other.y ? 1 : 0), + z + (other.z ? 1 : 0) + }; + } + + inline VoxelIndex operator*(const Vec3& other) const { + return VoxelIndex(std::floor(x * other.x), std::floor(y * other.y), std::floor(z * other.z)); + } + inline VoxelIndex operator*(float scalar) const { + return VoxelIndex(std::floor(x * scalar), std::floor(y * scalar), std::floor(z * scalar)); + } + + operator Vec3() const { + return Vec3(static_cast(x), static_cast(y), static_cast(z)); + } + + Vec3 toVec3() const { + return Vec3(static_cast(x), static_cast(y), static_cast(z)); + } + + // Hash function + size_t hash() const { + return std::hash{}(x) ^ + (std::hash{}(y) << 1) ^ + (std::hash{}(z) << 2); + } + + // Convert to string + std::string toString() const { + return "VoxelIndex(" + std::to_string(x) + ", " + + std::to_string(y) + ", " + + std::to_string(z) + ")"; + } + + // Comparison operators for completeness + bool operator==(const VoxelIndex& other) const { + return x == other.x && y == other.y && z == other.z; + } + + bool operator!=(const VoxelIndex& other) const { + return !(*this == other); + } +}; + +namespace std { + template<> + struct hash { + size_t operator()(const Vec3& p) const { + return hash()(p.x) ^ hash()(p.y) ^ hash()(p.z); + } + }; + template<> + struct hash { + size_t operator()(const VoxelIndex& idx) const { + return idx.hash(); + } + }; +} + +class VoxelGrid { +public: + // New structure - using position-based indexing + std::unordered_map pos_index_map; // Maps voxel center -> index in positions/colors + std::vector positions; // Voxel center positions + std::vector colors; // Average colors per voxel + float gridsize; // Voxel size + + // Old structure - to be removed/replaced + float voxel_size; + Vec3 min_bounds; + Vec3 max_bounds; + + // Grid dimensions + int dim_x, dim_y, dim_z; + + // Grid arrays for fast access (like Python) + std::vector grid_array; + std::vector color_array; + std::vector count_array; + + bool arrays_initialized = false; + + void initializeArrays() { + if (arrays_initialized) return; + + size_t total_size = dim_x * dim_y * dim_z; + grid_array.resize(total_size, false); + color_array.resize(total_size, Vec4{0, 0, 0, 0}); + count_array.resize(total_size, 0); + + // Populate arrays from the new data structure + for (const auto& [voxel_pos, index] : pos_index_map) { + VoxelIndex voxel_idx = getVoxelIndex(voxel_pos); + if (isValidIndex(voxel_idx)) { + size_t array_idx = getArrayIndex(voxel_idx); + grid_array[array_idx] = true; + color_array[array_idx] = colors[index]; + count_array[array_idx] = 1; // Each entry represents one voxel + } + } + + arrays_initialized = true; + } + + size_t getArrayIndex(const VoxelIndex& index) const { + return index[0] + dim_x * (index[1] + dim_y * index[2]); + } + + bool isValidIndex(const VoxelIndex& idx) const { + return idx[0] >= 0 && idx[0] < dim_x && + idx[1] >= 0 && idx[1] < dim_y && + idx[2] >= 0 && idx[2] < dim_z; + } + + VoxelGrid(float size = 0.1f) : gridsize(size), voxel_size(size), arrays_initialized(false) {} + + void addPoints(const std::vector& points, const std::vector& input_colors) { + if (points.size() != input_colors.size()) { + std::cerr << "Error: Points and colors vectors must have the same size" << std::endl; + return; + } + + if (points.empty()) return; + + // Calculate bounds + min_bounds = points[0]; + max_bounds = points[0]; + + for (const auto& point : points) { + min_bounds.x = std::min(min_bounds.x, point.x); + min_bounds.y = std::min(min_bounds.y, point.y); + min_bounds.z = std::min(min_bounds.z, point.z); + + max_bounds.x = std::max(max_bounds.x, point.x); + max_bounds.y = std::max(max_bounds.y, point.y); + max_bounds.z = std::max(max_bounds.z, point.z); + } + + // Calculate grid dimensions + Vec3 range = max_bounds - min_bounds; + dim_x = static_cast(std::ceil(range.x / gridsize)) + 1; + dim_y = static_cast(std::ceil(range.y / gridsize)) + 1; + dim_z = static_cast(std::ceil(range.z / gridsize)) + 1; + + // Temporary storage for averaging colors per voxel + std::unordered_map, Vec3::Hash> voxel_color_map; + + // Group points by voxel and collect colors + for (size_t i = 0; i < points.size(); ++i) { + Vec3 voxel_center = getVoxelCenter(getVoxelIndex(points[i])); + voxel_color_map[voxel_center].push_back(input_colors[i]); + } + + // Convert to the new structure - store average colors per voxel + positions.clear(); + colors.clear(); + pos_index_map.clear(); + + for (auto& [voxel_pos, color_list] : voxel_color_map) { + // Calculate average color + Vec4 avg_color{0, 0, 0, 0}; + for (const auto& color : color_list) { + avg_color.w += color.w; + avg_color.x += color.x; + avg_color.y += color.y; + avg_color.z += color.z; + } + avg_color.w /= color_list.size(); + avg_color.x /= color_list.size(); + avg_color.y /= color_list.size(); + avg_color.z /= color_list.size(); + + // Store in new structure + size_t index = positions.size(); + positions.push_back(voxel_pos); + colors.push_back(avg_color); + pos_index_map[voxel_pos] = index; + } + + // Initialize arrays for fast access + initializeArrays(); + } + + // Get voxel data using voxel center position + bool hasVoxelAt(const Vec3& voxel_center) const { + return pos_index_map.find(voxel_center) != pos_index_map.end(); + } + + Vec4 getVoxelColor(const Vec3& voxel_center) const { + auto it = pos_index_map.find(voxel_center); + if (it != pos_index_map.end()) { + return colors[it->second]; + } + return Vec4{0, 0, 0, 0}; + } + + // Fast array access using VoxelIndex (compatibility with existing code) + bool getVoxelOccupied(const VoxelIndex& voxel_idx) const { + if (!arrays_initialized || !isValidIndex(voxel_idx)) return false; + return grid_array[getArrayIndex(voxel_idx)]; + } + + Vec4 getVoxelColor(const VoxelIndex& voxel_idx) const { + if (!arrays_initialized || !isValidIndex(voxel_idx)) + return Vec4{0, 0, 0, 0}; + return color_array[getArrayIndex(voxel_idx)]; + } + + int getVoxelPointCount(const VoxelIndex& voxel_idx) const { + if (!arrays_initialized || !isValidIndex(voxel_idx)) return 0; + return count_array[getArrayIndex(voxel_idx)]; + } + + std::vector getVoxelIndices() const { + std::vector indices; + for (const auto& voxel_pos : positions) { + indices.push_back(getVoxelIndex(voxel_pos)); + } + return indices; + } + + std::vector getVoxelCenters() const { + return positions; + } + + size_t getNumVoxels() const { return positions.size(); } + size_t getNumPoints() const { + // Since we're storing averaged voxels, each position represents multiple original points + // For simplicity, return number of voxels + return positions.size(); + } + + Vec3 getMinBounds() const { return min_bounds; } + Vec3 getMaxBounds() const { return max_bounds; } + std::array getDimensions() const { return {dim_x, dim_y, dim_z}; } + float getVoxelSize() const { return gridsize; } + + VoxelIndex getVoxelIndex(const Vec3& point) const { + Vec3 normalized = point - min_bounds; + return VoxelIndex{ + static_cast(std::floor(normalized.x / gridsize)), + static_cast(std::floor(normalized.y / gridsize)), + static_cast(std::floor(normalized.z / gridsize)) + }; + } + + Vec3 getVoxelCenter(const VoxelIndex& voxel_idx) const { + return Vec3( + min_bounds.x + (voxel_idx[0] + 0.5f) * gridsize, + min_bounds.y + (voxel_idx[1] + 0.5f) * gridsize, + min_bounds.z + (voxel_idx[2] + 0.5f) * gridsize + ); + } + + void clear() { + pos_index_map.clear(); + positions.clear(); + colors.clear(); + grid_array.clear(); + color_array.clear(); + count_array.clear(); + arrays_initialized = false; + } + + void setVoxelSize(float size) { + gridsize = size; + voxel_size = size; + clear(); + } + + void printStats() const { + std::cout << "Voxel Grid Statistics:" << std::endl; + std::cout << " Voxel size: " << gridsize << std::endl; + std::cout << " Grid dimensions: " << dim_x << " x " << dim_y << " x " << dim_z << std::endl; + std::cout << " Number of voxels: " << getNumVoxels() << std::endl; + std::cout << " Using new position-based storage" << std::endl; + } +}; + +struct Image { + int width; + int height; + std::vector data; // RGBA format + + Image(int w, int h) : width(w), height(h), data(w * h * 4) { + // Initialize to white background + for (int i = 0; i < w * h * 4; i += 4) { + data[i] = 255; // R + data[i + 1] = 255; // G + data[i + 2] = 255; // B + data[i + 3] = 255; // A + } + } + + // Helper methods + uint8_t* pixel(int x, int y) { + return &data[(y * width + x) * 4]; + } + + void setPixel(int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) { + uint8_t* p = pixel(x, y); + p[0] = r; p[1] = g; p[2] = b; p[3] = a; + } + + bool saveAsBMP(const std::string& filename) { + #pragma pack(push, 1) + + // BMP file header (14 bytes) + struct BMPFileHeader { + uint16_t file_type{0x4D42}; // "BM" + uint32_t file_size{0}; + uint16_t reserved1{0}; + uint16_t reserved2{0}; + uint32_t offset_data{0}; + } file_header; + + // BMP info header (40 bytes) + struct BMPInfoHeader { + uint32_t size{40}; + int32_t width{0}; + int32_t height{0}; + uint16_t planes{1}; + uint16_t bit_count{32}; + uint32_t compression{0}; // BI_RGB + uint32_t size_image{0}; + int32_t x_pixels_per_meter{0}; + int32_t y_pixels_per_meter{0}; + uint32_t colors_used{0}; + uint32_t colors_important{0}; + } info_header; + + // Calculate sizes + uint32_t row_stride = width * 4; + uint32_t padding_size = (4 - (row_stride % 4)) % 4; + uint32_t data_size = (row_stride + padding_size) * height; + + file_header.file_size = sizeof(BMPFileHeader) + sizeof(BMPInfoHeader) + data_size; + file_header.offset_data = sizeof(BMPFileHeader) + sizeof(BMPInfoHeader); + + info_header.width = width; + info_header.height = -height; // Negative for top-down bitmap (no flipping needed) + info_header.size_image = data_size; + + // Open file + std::ofstream file(filename, std::ios::binary); + if (!file.is_open()) { + return false; + } + + // Write headers + file.write(reinterpret_cast(&file_header), sizeof(file_header)); + file.write(reinterpret_cast(&info_header), sizeof(info_header)); + + // Write pixel data (BMP stores as BGR, we have RGB) + std::vector row_buffer(row_stride + padding_size); + + for (int y = 0; y < height; ++y) { + uint8_t* row_start = &data[y * row_stride]; + + // Convert RGB to BGR and copy to buffer + for (int x = 0; x < width; ++x) { + uint8_t* src_pixel = row_start + (x * 4); + uint8_t* dst_pixel = row_buffer.data() + (x * 4); + + // Swap R and B channels (RGBA -> BGRA) + dst_pixel[0] = src_pixel[2]; // B + dst_pixel[1] = src_pixel[1]; // G + dst_pixel[2] = src_pixel[0]; // R + //dst_pixel[3] = src_pixel[3]; // A + } + + // Write row with padding + file.write(reinterpret_cast(row_buffer.data()), row_stride + padding_size); + } + + return file.good(); + } +}; + + +//noise functions +float fade(const float& a) { + TIME_FUNCTION; + return a * a * a * (10 + a * (-15 + a * 6)); +} + +float clamp(float x, float lowerlimit = 0.0f, float upperlimit = 1.0f) { + TIME_FUNCTION; + if (x < lowerlimit) return lowerlimit; + if (x > upperlimit) return upperlimit; + return x; +} + +float pascalTri(const float& a, const float& b) { + TIME_FUNCTION; + int result = 1; + for (int i = 0; i < b; ++i){ + result *= (a - 1) / (i + 1); + } + return result; +} + +float genSmooth(int N, float x) { + TIME_FUNCTION; + x = clamp(x, 0, 1); + float result = 0; + for (int n = 0; n <= N; ++n){ + result += pascalTri(-N - 1, n) * pascalTri(2 * N + 1, N-1) * pow(x, N + n + 1); + } + return result; +} + +float inverse_smoothstep(float x) { + TIME_FUNCTION; + return 0.5 - sin(asin(1.0 - 2.0 * x) / 3.0); +} + +float lerp(const float& t, const float& a, const float& b) { + TIME_FUNCTION; + return a + t * (b - a); +} + +float grad(const int& hash, const float& b, const float& c, const float& d) { + TIME_FUNCTION; + int h = hash & 15; + float u = (h < 8) ? c : b; + float v = (h < 4) ? b : ((h == 12 || h == 14) ? c : d); + return (((h & 1) == 0) ? u : -u) + (((h & 2) == 0) ? v : -v); +} + +float pnoise3d(const int p[512], const float& xf, const float& yf, const float& zf) { + TIME_FUNCTION; + int floorx = std::floor(xf); + int floory = std::floor(yf); + int floorz = std::floor(zf); + int iX = floorx & 255; + int iY = floory & 255; + int iZ = floorz & 255; + + int x = xf - floorx; + int y = yf - floory; + int z = zf - floorz; + + float u = fade(x); + float v = fade(y); + float w = fade(z); + + int A = p[iX] + iY; + int AA = p[A] + iZ; + int AB = p[A+1] + iZ; + + int B = p[iX + 1] + iY; + int BA = p[B] + iZ; + int BB = p[B+1] + iZ; + + float f = grad(p[BA], x-1, y, z); + float g = grad(p[AA], x, y, z); + float h = grad(p[BB], x-1, y-1, z); + float j = grad(p[BB], x-1, y-1, z); + float k = grad(p[AA+1], x, y, z-1); + float l = grad(p[BA+1], x-1, y, z-1); + float m = grad(p[AB+1], x, y-1, z-1); + float n = grad(p[BB+1], x-1, y-1, z-1); + + float o = lerp(u, m, n); + float q = lerp(u, k, l); + float r = lerp(u, h, j); + float s = lerp(u, f, g); + float t = lerp(v, q, o); + float e = lerp(v, s, r); + float d = lerp(w, e, t); + return d; +} + +std::tuple, std::vector> noiseBatch(int num_points, float scale, int sp[]) { + TIME_FUNCTION; + std::vector points; + std::vector colors; + points.reserve(num_points); + colors.reserve(num_points); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dis(-scale, scale); + + for (int i = 0; i < num_points; ++i) { + float x = dis(gen); + float y = dis(gen); + float z = dis(gen); + + float noise1 = pnoise3d(sp, (x * 0.5f), (y * 0.5f), (z * 0.5f)); + float noise2 = pnoise3d(sp, (x * 0.3f), (y * 0.3f), (z * 0.3f)); + float noise3 = pnoise3d(sp, (x * 0.7f), (y * 0.7f), (z * 0.7f)); + float noise4 = pnoise3d(sp, (x * 0.7f), (y * 0.7f), (z * 0.7f)); + + if (noise1 > 0.1f) { + float rt = (noise1 + 1.0f) * 0.5f; + float gt = (noise2 + 1.0f) * 0.5f; + float bt = (noise3 + 1.0f) * 0.5f; + float at = (noise4 + 1.0f) * 0.5f; + + float maxV = std::max({rt, gt, bt}); + if (maxV > 0) { + float r = rt / maxV; + float g = gt / maxV; + float b = bt / maxV; + float a = at / maxV; + points.push_back({x, y, z}); + colors.push_back({r, g, b, a}); + } + } + } + return std::make_tuple(points, colors); +} + +std::tuple, std::vector> genPointCloud(int numP, float scale, int seed) { + TIME_FUNCTION; + int permutation[256]; + for (int i = 0; i < 256; ++i) { + permutation[i] = i; + } + std::mt19937 rng(seed); + std::shuffle(permutation, permutation+256, rng); + int p[512]; + for (int i = 0; i < 256; ++i) { + p[i] = permutation[i]; + p[i + 256] = permutation[i]; + } + return noiseBatch(numP, scale, p); +} + + +//voxel stuff +Bool3 greaterThanZero(const Vec3& v) { + return {v.x > 0, v.y > 0, v.z > 0}; +} + +Image render(int height, int width, Vec3 forward, Vec3 right, Vec3 up, Vec3 rayOrigin, Vec3 vbound, + float vsize, VoxelGrid grid, int dims) { + TIME_FUNCTION; + + Image img = Image(height, width); + + float max_t = 50.0f; + int max_steps = 123; + float max_dist = 25.0f; + float screen_height = height; + float screen_width = width; + + float inv_w = 1.0f / width; + float inv_h = 1.0f / height; + float scr_w_half = screen_width * 0.5f; + float scr_h_half = screen_height * 0.5f; + + std::cout << height << std::endl; + std::cout << width << std::endl; + for (int y = 0; y < height; y++) { + int sy = std::ceil(1.0 - ((2.0 * y) * inv_h) * scr_h_half); + for (int x = 0; x < width; x++) { + int sx = std::ceil((((2.0 * x) * inv_w) - 1.0) * scr_w_half); + std::cout << "working in: " << x << ", " << y << std::endl; + Vec3 ray_dir = (forward + (sx * right) + (sy * up)).normalize(); + std::cout << "current ray direction: " << ray_dir.toString() << std::endl; + Vec3 cv1 = ((rayOrigin - vbound) / vsize); + VoxelIndex cv = VoxelIndex(static_cast(cv1.x), static_cast(cv1.y), static_cast(cv1.z)); + std::cout << "cell at: " << cv.toString() << std::endl; + Vec3 inv_dir = Vec3( + ray_dir.x != 0 ? 1.0f / ray_dir.x : std::numeric_limits::max(), + ray_dir.y != 0 ? 1.0f / ray_dir.y : std::numeric_limits::max(), + ray_dir.z != 0 ? 1.0f / ray_dir.z : std::numeric_limits::max() + ); + std::cout << "inverse of the current ray: " << inv_dir.toString() << std::endl; + Vec3 step = ray_dir.sign(); + std::cout << "current ray signs: " << step.toString() << std::endl; + Bool3 step_mask = greaterThanZero(step); + VoxelIndex next_voxel_bound = (cv + step_mask) * vsize + vbound; + std::cout << "next cell at: " << next_voxel_bound.toString() << std::endl; + Vec3 t_max = (Vec3(next_voxel_bound) - rayOrigin) * inv_dir; + std::cout << "t_max at: " << t_max.toString() << std::endl; + Vec3 t_delta = vsize / inv_dir.abs(); + std::cout << "t_delta: " << t_delta.toString() << std::endl; + float t = 0.0f; + + Vec4 accumulatedColor = Vec4(0,0,0,0); + //std::cout << x << "," << y << std::endl; + for (int iter = 0; iter < max_steps; iter++) { + if (max_t < t) { + std::cout << "t failed " << std::endl; + break; + } + if (accumulatedColor.z >= 1.0) { + std::cout << "z failed " << std::endl; + break; + } + + if (cv.x >= 0 && cv.x < dims && + cv.y >= 0 && cv.y < dims && + cv.z >= 0 && cv.z < dims) { + std::cout << "found cell at: " << cv.toString() << std::endl; + if (grid.getVoxelOccupied(cv)) { + std::cout << "found occupied cell at: " << cv.toString() << std::endl; + Vec4 voxel_color = grid.getVoxelColor(cv); + + float weight = voxel_color.z * (1.0f - accumulatedColor.z); + accumulatedColor.w += voxel_color.w * weight; + accumulatedColor.x += voxel_color.x * weight; + accumulatedColor.y += voxel_color.y * weight; + accumulatedColor.z += voxel_color.z * weight; + } + + int minAxis = 0; + if (t_max.y < t_max.x) minAxis = 1; + if (t_max.z < t_max[minAxis]) minAxis = 2; + cv[minAxis] += step[minAxis]; + t = t_max[minAxis]; + t_max[minAxis] += t_delta[minAxis]; + } + } + if (accumulatedColor.z > 0) { + std::cout << "setting a color at " << x << " and " << y << std::endl; + float r = accumulatedColor.w + (1.0f - accumulatedColor.z) * 1.0f; + float g = accumulatedColor.x + (1.0f - accumulatedColor.z) * 1.0f; + float b = accumulatedColor.y + (1.0f - accumulatedColor.z) * 1.0f; + + img.setPixel(x, y, + static_cast(r * 255), + static_cast(g * 255), + static_cast(b * 255), + 255); + } + + } + } + return img; +} + +int main() { + std::cout << "Generating point cloud" << std::endl; + auto [points, colors] = genPointCloud(150000, 10.0, 43); + std::cout << "Generating done" << std::endl; + + + VoxelGrid voxel_grid(0.2f); + + std::cout << "Adding points to voxel grid..." << std::endl; + voxel_grid.addPoints(points, colors); + voxel_grid.printStats(); + + // Use the voxel grid's actual bounds + Vec3 min_bounds = voxel_grid.getMinBounds(); + Vec3 max_bounds = voxel_grid.getMaxBounds(); + Vec3 grid_center = (min_bounds + max_bounds) * 0.5; + + // Proper camera setup + Vec3 rayOrigin(0, 0, 15); + Vec3 forward = (grid_center - rayOrigin).normalize(); + Vec3 up(0, 1, 0); + Vec3 right = forward.cross(up).normalize(); + auto dims = voxel_grid.getDimensions(); + int max_dim = std::max({dims[0], dims[1], dims[2]}); + + Image img = render(50, 50, forward, right, up, rayOrigin, min_bounds, + voxel_grid.getVoxelSize(), voxel_grid, max_dim); + img.saveAsBMP("cpp_voxel_render.bmp"); + + FunctionTimer::printStats(FunctionTimer::Mode::ENHANCED); + return 0; +} diff --git a/d.cpp b/d.cpp new file mode 100644 index 0000000..11382a3 --- /dev/null +++ b/d.cpp @@ -0,0 +1,659 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "timing_decorator.hpp" +#include +#include + + +// Forward declarations +class Vec3; +class Vec4; + +class Vec3 { +public: + float x, y, z; + + // Constructors + Vec3() : x(0), y(0), z(0) {} + Vec3(float x, float y, float z) : x(x), y(y), z(z) {} + + // Arithmetic operations + Vec3 operator+(const Vec3& other) const { + return Vec3(x + other.x, y + other.y, z + other.z); + } + + Vec3 operator-(const Vec3& other) const { + return Vec3(x - other.x, y - other.y, z - other.z); + } + + Vec3 operator*(float scalar) const { + return Vec3(x * scalar, y * scalar, z * scalar); + } + + Vec3 operator/(float scalar) const { + return Vec3(x / scalar, y / scalar, z / scalar); + } + + // Dot product + float dot(const Vec3& other) const { + return x * other.x + y * other.y + z * other.z; + } + + // Cross product + Vec3 cross(const Vec3& other) const { + return Vec3( + y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x + ); + } + + // Length and normalization + float length() const { + return std::sqrt(x * x + y * y + z * z); + } + + Vec3 normalized() const { + float len = length(); + if (len > 0) { + return *this / len; + } + return *this; + } + + // Component-wise absolute value + Vec3 abs() const { + return Vec3(std::abs(x), std::abs(y), std::abs(z)); + } + + // Component-wise floor + Vec3 floor() const { + return Vec3(std::floor(x), std::floor(y), std::floor(z)); + } + + // Equality operator for use in hash maps + bool operator==(const Vec3& other) const { + return x == other.x && y == other.y && z == other.z; + } + + inline Vec3 operator*(const Vec3& scalar) const { + return Vec3(x * scalar.x, y * scalar.y, z * scalar.z); + } + + inline Vec3 operator/(const Vec3& scalar) const { + return Vec3(x / scalar.x, y / scalar.y, z / scalar.z); + } +}; + +// Hash function for Vec3 to use in unordered_map +namespace std { + template<> + struct hash { + size_t operator()(const Vec3& v) const { + return hash()(v.x) ^ (hash()(v.y) << 1) ^ (hash()(v.z) << 2); + } + }; +} + +class Vec4 { +public: + float r, g, b, a; + + // Constructors + Vec4() : r(0), g(0), b(0), a(1.0f) {} + Vec4(float r, float g, float b, float a = 1.0f) : r(r), g(g), b(b), a(a) {} + + // Construct from Vec3 with alpha + Vec4(const Vec3& rgb, float a = 1.0f) : r(rgb.x), g(rgb.y), b(rgb.z), a(a) {} + + // Arithmetic operations + Vec4 operator+(const Vec4& other) const { + return Vec4(r + other.r, g + other.g, b + other.b, a + other.a); + } + + Vec4 operator-(const Vec4& other) const { + return Vec4(r - other.r, g - other.g, b - other.b, a - other.a); + } + + Vec4 operator*(float scalar) const { + return Vec4(r * scalar, g * scalar, b * scalar, a * scalar); + } + + Vec4 operator/(float scalar) const { + return Vec4(r / scalar, g / scalar, b / scalar, a / scalar); + } + + // Component-wise multiplication + Vec4 operator*(const Vec4& other) const { + return Vec4(r * other.r, g * other.g, b * other.b, a * other.a); + } + + // Clamp values between 0 and 1 + Vec4 clamped() const { + return Vec4( + std::clamp(r, 0.0f, 1.0f), + std::clamp(g, 0.0f, 1.0f), + std::clamp(b, 0.0f, 1.0f), + std::clamp(a, 0.0f, 1.0f) + ); + } + + // Convert to Vec3 (ignoring alpha) + Vec3 toVec3() const { + return Vec3(r, g, b); + } + + // Convert to 8-bit color values + void toUint8(uint8_t& red, uint8_t& green, uint8_t& blue, uint8_t& alpha) const { + red = static_cast(std::clamp(r, 0.0f, 1.0f) * 255); + green = static_cast(std::clamp(g, 0.0f, 1.0f) * 255); + blue = static_cast(std::clamp(b, 0.0f, 1.0f) * 255); + alpha = static_cast(std::clamp(a, 0.0f, 1.0f) * 255); + } + + void toUint8(uint8_t& red, uint8_t& green, uint8_t& blue) const { + red = static_cast(std::clamp(r, 0.0f, 1.0f) * 255); + green = static_cast(std::clamp(g, 0.0f, 1.0f) * 255); + blue = static_cast(std::clamp(b, 0.0f, 1.0f) * 255); + } +}; + +class VoxelGrid { +private: + std::unordered_map positionToIndex; + std::vector positions; + std::vector colors; + + Vec3 gridSize; + +public: + Vec3 voxelSize; + VoxelGrid(const Vec3& size, const Vec3& voxelSize = Vec3(1, 1, 1)) + : gridSize(size), voxelSize(voxelSize) {} + + // Add a voxel at position with color + void addVoxel(const Vec3& position, const Vec4& color) { + Vec3 gridPos = worldToGrid(position); + + // Check if voxel already exists + auto it = positionToIndex.find(gridPos); + if (it == positionToIndex.end()) { + // New voxel + size_t index = positions.size(); + positions.push_back(gridPos); + colors.push_back(color); + positionToIndex[gridPos] = index; + } else { + // Update existing voxel (you might want to blend colors instead) + colors[it->second] = color; + } + } + + // Get voxel color at position + Vec4 getVoxel(const Vec3& position) const { + Vec3 gridPos = worldToGrid(position); + auto it = positionToIndex.find(gridPos); + if (it != positionToIndex.end()) { + return colors[it->second]; + } + return Vec4(0, 0, 0, 0); // Transparent black for empty voxels + } + + // Check if position is occupied + bool isOccupied(const Vec3& position) const { + Vec3 gridPos = worldToGrid(position); + return positionToIndex.find(gridPos) != positionToIndex.end(); + } + + // Convert world coordinates to grid coordinates + Vec3 worldToGrid(const Vec3& worldPos) const { + return (worldPos / voxelSize).floor(); + } + + // Convert grid coordinates to world coordinates + Vec3 gridToWorld(const Vec3& gridPos) const { + return gridPos * voxelSize; + } + + // Get all occupied positions + const std::vector& getOccupiedPositions() const { + return positions; + } + + // Get all colors + const std::vector& getColors() const { + return colors; + } + + // Get the mapping from position to index + const std::unordered_map& getPositionToIndexMap() const { + return positionToIndex; + } + + // Get grid size + const Vec3& getGridSize() const { + return gridSize; + } + + // Get voxel size + const Vec3& getVoxelSize() const { + return voxelSize; + } + + // Clear the grid + void clear() { + positions.clear(); + colors.clear(); + positionToIndex.clear(); + } +}; + +class AmanatidesWooAlgorithm { +public: + struct Ray { + Vec3 origin; + Vec3 direction; + float tMax; + + Ray(const Vec3& origin, const Vec3& direction, float tMax = 1000.0f) + : origin(origin), direction(direction.normalized()), tMax(tMax) {} + }; + + struct TraversalState { + Vec3 currentVoxel; + Vec3 tMax; + Vec3 tDelta; + Vec3 step; + bool hit; + float t; + + TraversalState() : hit(false), t(0) {} + }; + + // Initialize traversal state for a ray + static TraversalState initTraversal(const Ray& ray, const Vec3& voxelSize) { + TraversalState state; + + // Find the starting voxel + Vec3 startPos = ray.origin / voxelSize; + state.currentVoxel = startPos.floor(); + + // Determine step directions and initialize tMax + Vec3 rayDir = ray.direction; + Vec3 invDir = Vec3( + rayDir.x != 0 ? 1.0f / rayDir.x : std::numeric_limits::max(), + rayDir.y != 0 ? 1.0f / rayDir.y : std::numeric_limits::max(), + rayDir.z != 0 ? 1.0f / rayDir.z : std::numeric_limits::max() + ); + + // Calculate step directions + state.step = Vec3( + rayDir.x > 0 ? 1 : (rayDir.x < 0 ? -1 : 0), + rayDir.y > 0 ? 1 : (rayDir.y < 0 ? -1 : 0), + rayDir.z > 0 ? 1 : (rayDir.z < 0 ? -1 : 0) + ); + + // Calculate tMax for each axis + Vec3 nextVoxelBoundary = state.currentVoxel; + if (state.step.x > 0) nextVoxelBoundary.x += 1; + if (state.step.y > 0) nextVoxelBoundary.y += 1; + if (state.step.z > 0) nextVoxelBoundary.z += 1; + + state.tMax = Vec3( + (nextVoxelBoundary.x - startPos.x) * invDir.x, + (nextVoxelBoundary.y - startPos.y) * invDir.y, + (nextVoxelBoundary.z - startPos.z) * invDir.z + ); + + // Calculate tDelta + state.tDelta = Vec3( + state.step.x * invDir.x, + state.step.y * invDir.y, + state.step.z * invDir.z + ); + + state.hit = false; + state.t = 0; + + return state; + } + + // Traverse the grid along the ray + static bool traverse(const Ray& ray, const VoxelGrid& grid, + std::vector& hitVoxels, std::vector& hitDistances, + int maxSteps = 1000) { + TraversalState state = initTraversal(ray, grid.voxelSize); + Vec3 voxelSize = grid.voxelSize; + + hitVoxels.clear(); + hitDistances.clear(); + + for (int step = 0; step < maxSteps; step++) { + // Check if current voxel is occupied + Vec3 worldPos = grid.gridToWorld(state.currentVoxel); + if (grid.isOccupied(worldPos)) { + hitVoxels.push_back(state.currentVoxel); + hitDistances.push_back(state.t); + } + + // Find next voxel + if (state.tMax.x < state.tMax.y) { + if (state.tMax.x < state.tMax.z) { + state.currentVoxel.x += state.step.x; + state.t = state.tMax.x; + state.tMax.x += state.tDelta.x; + } else { + state.currentVoxel.z += state.step.z; + state.t = state.tMax.z; + state.tMax.z += state.tDelta.z; + } + } else { + if (state.tMax.y < state.tMax.z) { + state.currentVoxel.y += state.step.y; + state.t = state.tMax.y; + state.tMax.y += state.tDelta.y; + } else { + state.currentVoxel.z += state.step.z; + state.t = state.tMax.z; + state.tMax.z += state.tDelta.z; + } + } + + // Check if we've exceeded maximum distance + if (state.t > ray.tMax) { + break; + } + } + + return !hitVoxels.empty(); + } +}; + +class BMPWriter { +private: + #pragma pack(push, 1) + struct BMPHeader { + uint16_t signature = 0x4D42; // "BM" + uint32_t fileSize; + uint16_t reserved1 = 0; + uint16_t reserved2 = 0; + uint32_t dataOffset = 54; + }; + + struct BMPInfoHeader { + uint32_t headerSize = 40; + int32_t width; + int32_t height; + uint16_t planes = 1; + uint16_t bitsPerPixel = 24; + uint32_t compression = 0; + uint32_t imageSize; + int32_t xPixelsPerMeter = 0; + int32_t yPixelsPerMeter = 0; + uint32_t colorsUsed = 0; + uint32_t importantColors = 0; + }; + #pragma pack(pop) + +public: + static bool saveBMP(const std::string& filename, const std::vector& pixels, int width, int height) { + BMPHeader header; + BMPInfoHeader infoHeader; + + int rowSize = (width * 3 + 3) & ~3; // 24-bit, padded to 4 bytes + int imageSize = rowSize * height; + + header.fileSize = sizeof(BMPHeader) + sizeof(BMPInfoHeader) + imageSize; + infoHeader.width = width; + infoHeader.height = height; + infoHeader.imageSize = imageSize; + + std::ofstream file(filename, std::ios::binary); + if (!file) { + return false; + } + + file.write(reinterpret_cast(&header), sizeof(header)); + file.write(reinterpret_cast(&infoHeader), sizeof(infoHeader)); + + // Write pixel data (BMP stores pixels bottom-to-top) + std::vector row(rowSize); + for (int y = height - 1; y >= 0; --y) { + const uint8_t* src = &pixels[y * width * 3]; + std::memcpy(row.data(), src, width * 3); + file.write(reinterpret_cast(row.data()), rowSize); + } + + return true; + } + + static bool saveVoxelGridSlice(const std::string& filename, const VoxelGrid& grid, int sliceZ) { + Vec3 gridSize = grid.getGridSize(); + int width = static_cast(gridSize.x); + int height = static_cast(gridSize.y); + + std::vector pixels(width * height * 3, 0); + + // Render the slice + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + Vec3 worldPos = grid.gridToWorld(Vec3(x, y, sliceZ)); + Vec4 color = grid.getVoxel(worldPos); + + int index = (y * width + x) * 3; + color.toUint8(pixels[index + 2], pixels[index + 1], pixels[index]); // BMP is BGR + } + } + + return saveBMP(filename, pixels, width, height); + } + + static bool saveRayTraceResults(const std::string& filename, const VoxelGrid& grid, + const std::vector& hitVoxels, + const AmanatidesWooAlgorithm::Ray& ray, + int width = 800, int height = 600) { + std::vector pixels(width * height * 3, 0); + + // Background color (dark gray) + for (int i = 0; i < width * height * 3; i += 3) { + pixels[i] = 50; // B + pixels[i + 1] = 50; // G + pixels[i + 2] = 50; // R + } + + // Draw the grid bounds + Vec3 gridSize = grid.getGridSize(); + drawGrid(pixels, width, height, gridSize); + + // Draw hit voxels + for (const auto& voxel : hitVoxels) { + drawVoxel(pixels, width, height, voxel, gridSize, Vec4(1, 0, 0, 1)); // Red for hit voxels + } + + // Draw the ray + drawRay(pixels, width, height, ray, gridSize); + + return saveBMP(filename, pixels, width, height); + } + +private: + static void drawGrid(std::vector& pixels, int width, int height, const Vec3& gridSize) { + // Draw grid boundaries in white + for (int x = 0; x <= gridSize.x; ++x) { + int screenX = mapToScreenX(x, gridSize.x, width); + for (int y = 0; y < height; ++y) { + int index = (y * width + screenX) * 3; + if (y % 5 == 0) { // Dotted line + pixels[index] = 255; // B + pixels[index + 1] = 255; // G + pixels[index + 2] = 255; // R + } + } + } + + for (int y = 0; y <= gridSize.y; ++y) { + int screenY = mapToScreenY(y, gridSize.y, height); + for (int x = 0; x < width; ++x) { + int index = (screenY * width + x) * 3; + if (x % 5 == 0) { // Dotted line + pixels[index] = 255; // B + pixels[index + 1] = 255; // G + pixels[index + 2] = 255; // R + } + } + } + } + + static void drawVoxel(std::vector& pixels, int width, int height, + const Vec3& voxel, const Vec3& gridSize, const Vec4& color) { + int screenX = mapToScreenX(voxel.x, gridSize.x, width); + int screenY = mapToScreenY(voxel.y, gridSize.y, height); + + uint8_t r, g, b; + color.toUint8(r, g, b); + + // Draw a 4x4 square for the voxel + for (int dy = -2; dy <= 2; ++dy) { + for (int dx = -2; dx <= 2; ++dx) { + int px = screenX + dx; + int py = screenY + dy; + if (px >= 0 && px < width && py >= 0 && py < height) { + int index = (py * width + px) * 3; + pixels[index] = b; + pixels[index + 1] = g; + pixels[index + 2] = r; + } + } + } + } + + static void drawRay(std::vector& pixels, int width, int height, + const AmanatidesWooAlgorithm::Ray& ray, const Vec3& gridSize) { + // Draw ray origin and direction + int originX = mapToScreenX(ray.origin.x, gridSize.x, width); + int originY = mapToScreenY(ray.origin.y, gridSize.y, height); + + // Draw ray origin (green) + for (int dy = -3; dy <= 3; ++dy) { + for (int dx = -3; dx <= 3; ++dx) { + int px = originX + dx; + int py = originY + dy; + if (px >= 0 && px < width && py >= 0 && py < height) { + int index = (py * width + px) * 3; + pixels[index] = 0; // B + pixels[index + 1] = 255; // G + pixels[index + 2] = 0; // R + } + } + } + + // Draw ray direction (yellow line) + Vec3 endPoint = ray.origin + ray.direction * 10.0f; // Extend ray + int endX = mapToScreenX(endPoint.x, gridSize.x, width); + int endY = mapToScreenY(endPoint.y, gridSize.y, height); + + drawLine(pixels, width, height, originX, originY, endX, endY, Vec4(1, 1, 0, 1)); + } + + static void drawLine(std::vector& pixels, int width, int height, + int x0, int y0, int x1, int y1, const Vec4& color) { + uint8_t r, g, b; + color.toUint8(r, g, b); + + int dx = std::abs(x1 - x0); + int dy = std::abs(y1 - y0); + int sx = (x0 < x1) ? 1 : -1; + int sy = (y0 < y1) ? 1 : -1; + int err = dx - dy; + + while (true) { + if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) { + int index = (y0 * width + x0) * 3; + pixels[index] = b; + pixels[index + 1] = g; + pixels[index + 2] = r; + } + + if (x0 == x1 && y0 == y1) break; + + int e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + } + + static int mapToScreenX(float x, float gridWidth, int screenWidth) { + return static_cast((x / gridWidth) * screenWidth); + } + + static int mapToScreenY(float y, float gridHeight, int screenHeight) { + return static_cast((y / gridHeight) * screenHeight); + } +}; + +// Example usage +int main() { + // Create a voxel grid + VoxelGrid grid(Vec3(10, 10, 10), Vec3(1, 1, 1)); + + // Add some voxels + grid.addVoxel(Vec3(1, 1, 1), Vec4(1, 0, 0, 1)); // Red + grid.addVoxel(Vec3(2, 2, 2), Vec4(0, 1, 0, 0.5f)); // Green with transparency + grid.addVoxel(Vec3(3, 3, 3), Vec4(0, 0, 1, 1)); // Blue + grid.addVoxel(Vec3(4, 4, 4), Vec4(1, 1, 0, 1)); // Yellow + grid.addVoxel(Vec3(5, 5, 5), Vec4(1, 0, 1, 1)); // Magenta + + // Create a ray + AmanatidesWooAlgorithm::Ray ray(Vec3(0, 0, 0), Vec3(1, 1, 1).normalized()); + + // Traverse the grid + std::vector hitVoxels; + std::vector hitDistances; + bool hit = AmanatidesWooAlgorithm::traverse(ray, grid, hitVoxels, hitDistances); + + if (hit) { + printf("Ray hit %zu voxels:\n", hitVoxels.size()); + for (size_t i = 0; i < hitVoxels.size(); i++) { + Vec4 color = grid.getVoxel(grid.gridToWorld(hitVoxels[i])); + printf(" Voxel at (%.1f, %.1f, %.1f), distance: %.2f, color: (%.1f, %.1f, %.1f, %.1f)\n", + hitVoxels[i].x, hitVoxels[i].y, hitVoxels[i].z, + hitDistances[i], + color.r, color.g, color.b, color.a); + } + } + + // Save results to BMP files + printf("\nSaving results to BMP files...\n"); + + // Save a slice of the voxel grid + if (BMPWriter::saveVoxelGridSlice("voxel_grid_slice.bmp", grid, 1)) { + printf("Saved voxel grid slice to 'voxel_grid_slice.bmp'\n"); + } else { + printf("Failed to save voxel grid slice\n"); + } + + // Save ray tracing visualization + if (BMPWriter::saveRayTraceResults("ray_trace_results.bmp", grid, hitVoxels, ray)) { + printf("Saved ray trace results to 'ray_trace_results.bmp'\n"); + } else { + printf("Failed to save ray trace results\n"); + } + + return 0; +} \ No newline at end of file diff --git a/e.cpp b/e.cpp new file mode 100644 index 0000000..0397384 --- /dev/null +++ b/e.cpp @@ -0,0 +1,854 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "timing_decorator.hpp" +#include "util/vec3.hpp" +#include "util/vec4.hpp" + +class VoxelGrid { +private: + std::unordered_map positionToIndex; + std::vector positions; + std::vector colors; + + Vec3 gridSize; + +public: + Vec3 voxelSize; + VoxelGrid(const Vec3& size, const Vec3& voxelSize = Vec3(1, 1, 1)) + : gridSize(size), voxelSize(voxelSize) {} + + // Add a voxel at position with color + void addVoxel(const Vec3& position, const Vec4& color) { + Vec3 gridPos = worldToGrid(position); + + // Check if voxel already exists + auto it = positionToIndex.find(gridPos); + if (it == positionToIndex.end()) { + // New voxel + size_t index = positions.size(); + positions.push_back(gridPos); + colors.push_back(color); + positionToIndex[gridPos] = index; + } else { + // Update existing voxel (you might want to blend colors instead) + colors[it->second] = color; + } + } + + // Get voxel color at position + Vec4 getVoxel(const Vec3& position) const { + Vec3 gridPos = worldToGrid(position); + auto it = positionToIndex.find(gridPos); + if (it != positionToIndex.end()) { + return colors[it->second]; + } + return Vec4(0, 0, 0, 0); // Transparent black for empty voxels + } + + // Check if position is occupied + bool isOccupied(const Vec3& position) const { + Vec3 gridPos = worldToGrid(position); + return positionToIndex.find(gridPos) != positionToIndex.end(); + } + + // Convert world coordinates to grid coordinates + Vec3 worldToGrid(const Vec3& worldPos) const { + return (worldPos / voxelSize).floor(); + } + + // Convert grid coordinates to world coordinates + Vec3 gridToWorld(const Vec3& gridPos) const { + return gridPos * voxelSize; + } + + // Get all occupied positions + const std::vector& getOccupiedPositions() const { + return positions; + } + + // Get all colors + const std::vector& getColors() const { + return colors; + } + + // Get the mapping from position to index + const std::unordered_map& getPositionToIndexMap() const { + return positionToIndex; + } + + // Get grid size + const Vec3& getGridSize() const { + return gridSize; + } + + // Get voxel size + const Vec3& getVoxelSize() const { + return voxelSize; + } + + // Clear the grid + void clear() { + positions.clear(); + colors.clear(); + positionToIndex.clear(); + } +}; + +class AmanatidesWooAlgorithm { +public: + struct Ray { + Vec3 origin; + Vec3 direction; + float tMax; + + Ray(const Vec3& origin, const Vec3& direction, float tMax = 1000.0f) + : origin(origin), direction(direction.normalized()), tMax(tMax) {} + }; + + struct TraversalState { + Vec3 currentVoxel; + Vec3 tMax; + Vec3 tDelta; + Vec3 step; + bool hit; + float t; + + TraversalState() : hit(false), t(0) {} + }; + + // Initialize traversal state for a ray + static TraversalState initTraversal(const Ray& ray, const Vec3& voxelSize) { + TraversalState state; + + // Find the starting voxel + Vec3 startPos = ray.origin / voxelSize; + state.currentVoxel = startPos.floor(); + + // Determine step directions and initialize tMax + Vec3 rayDir = ray.direction; + Vec3 invDir = Vec3( + rayDir.x != 0 ? 1.0f / rayDir.x : std::numeric_limits::max(), + rayDir.y != 0 ? 1.0f / rayDir.y : std::numeric_limits::max(), + rayDir.z != 0 ? 1.0f / rayDir.z : std::numeric_limits::max() + ); + + // Calculate step directions + state.step = Vec3( + rayDir.x > 0 ? 1 : (rayDir.x < 0 ? -1 : 0), + rayDir.y > 0 ? 1 : (rayDir.y < 0 ? -1 : 0), + rayDir.z > 0 ? 1 : (rayDir.z < 0 ? -1 : 0) + ); + + // Calculate tMax for each axis + Vec3 nextVoxelBoundary = state.currentVoxel; + if (state.step.x > 0) nextVoxelBoundary.x += 1; + if (state.step.y > 0) nextVoxelBoundary.y += 1; + if (state.step.z > 0) nextVoxelBoundary.z += 1; + + state.tMax = Vec3( + (nextVoxelBoundary.x - startPos.x) * invDir.x, + (nextVoxelBoundary.y - startPos.y) * invDir.y, + (nextVoxelBoundary.z - startPos.z) * invDir.z + ); + + // Calculate tDelta + state.tDelta = Vec3( + state.step.x * invDir.x, + state.step.y * invDir.y, + state.step.z * invDir.z + ); + + state.hit = false; + state.t = 0; + + return state; + } + + // Traverse the grid along the ray + static bool traverse(const Ray& ray, const VoxelGrid& grid, + std::vector& hitVoxels, std::vector& hitDistances, + int maxSteps = 1000) { + TraversalState state = initTraversal(ray, grid.voxelSize); + Vec3 voxelSize = grid.voxelSize; + + hitVoxels.clear(); + hitDistances.clear(); + + for (int step = 0; step < maxSteps; step++) { + // Check if current voxel is occupied + Vec3 worldPos = grid.gridToWorld(state.currentVoxel); + if (grid.isOccupied(worldPos)) { + hitVoxels.push_back(state.currentVoxel); + hitDistances.push_back(state.t); + } + + // Find next voxel + if (state.tMax.x < state.tMax.y) { + if (state.tMax.x < state.tMax.z) { + state.currentVoxel.x += state.step.x; + state.t = state.tMax.x; + state.tMax.x += state.tDelta.x; + } else { + state.currentVoxel.z += state.step.z; + state.t = state.tMax.z; + state.tMax.z += state.tDelta.z; + } + } else { + if (state.tMax.y < state.tMax.z) { + state.currentVoxel.y += state.step.y; + state.t = state.tMax.y; + state.tMax.y += state.tDelta.y; + } else { + state.currentVoxel.z += state.step.z; + state.t = state.tMax.z; + state.tMax.z += state.tDelta.z; + } + } + + // Check if we've exceeded maximum distance + if (state.t > ray.tMax) { + break; + } + } + + return !hitVoxels.empty(); + } +}; + +class BMPWriter { +private: + #pragma pack(push, 1) + struct BMPHeader { + uint16_t signature = 0x4D42; // "BM" + uint32_t fileSize; + uint16_t reserved1 = 0; + uint16_t reserved2 = 0; + uint32_t dataOffset = 54; + }; + + struct BMPInfoHeader { + uint32_t headerSize = 40; + int32_t width; + int32_t height; + uint16_t planes = 1; + uint16_t bitsPerPixel = 24; + uint32_t compression = 0; + uint32_t imageSize; + int32_t xPixelsPerMeter = 0; + int32_t yPixelsPerMeter = 0; + uint32_t colorsUsed = 0; + uint32_t importantColors = 0; + }; + #pragma pack(pop) + +public: + static bool saveBMP(const std::string& filename, const std::vector& pixels, int width, int height) { + BMPHeader header; + BMPInfoHeader infoHeader; + + int rowSize = (width * 3 + 3) & ~3; // 24-bit, padded to 4 bytes + int imageSize = rowSize * height; + + header.fileSize = sizeof(BMPHeader) + sizeof(BMPInfoHeader) + imageSize; + infoHeader.width = width; + infoHeader.height = height; + infoHeader.imageSize = imageSize; + + std::ofstream file(filename, std::ios::binary); + if (!file) { + return false; + } + + file.write(reinterpret_cast(&header), sizeof(header)); + file.write(reinterpret_cast(&infoHeader), sizeof(infoHeader)); + + // Write pixel data (BMP stores pixels bottom-to-top) + std::vector row(rowSize); + for (int y = height - 1; y >= 0; --y) { + const uint8_t* src = &pixels[y * width * 3]; + std::memcpy(row.data(), src, width * 3); + file.write(reinterpret_cast(row.data()), rowSize); + } + + return true; + } + + static bool saveVoxelGridSlice(const std::string& filename, const VoxelGrid& grid, int sliceZ) { + Vec3 gridSize = grid.getGridSize(); + int width = static_cast(gridSize.x); + int height = static_cast(gridSize.y); + + std::vector pixels(width * height * 3, 0); + + // Render the slice + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + Vec3 worldPos = grid.gridToWorld(Vec3(x, y, sliceZ)); + Vec4 color = grid.getVoxel(worldPos); + + int index = (y * width + x) * 3; + color.toUint8(pixels[index + 2], pixels[index + 1], pixels[index]); // BMP is BGR + } + } + + return saveBMP(filename, pixels, width, height); + } + + static bool saveRayTraceResults(const std::string& filename, const VoxelGrid& grid, + const std::vector& hitVoxels, + const AmanatidesWooAlgorithm::Ray& ray, + int width = 800, int height = 600) { + std::vector pixels(width * height * 3, 0); + + // Background color (dark gray) + for (int i = 0; i < width * height * 3; i += 3) { + pixels[i] = 50; // B + pixels[i + 1] = 50; // G + pixels[i + 2] = 50; // R + } + + // Draw the grid bounds + Vec3 gridSize = grid.getGridSize(); + drawGrid(pixels, width, height, gridSize); + + // Draw hit voxels + for (const auto& voxel : hitVoxels) { + drawVoxel(pixels, width, height, voxel, gridSize, Vec4(1, 0, 0, 1)); // Red for hit voxels + } + + // Draw the ray + drawRay(pixels, width, height, ray, gridSize); + + return saveBMP(filename, pixels, width, height); + } + +private: + static void drawGrid(std::vector& pixels, int width, int height, const Vec3& gridSize) { + // Draw grid boundaries in white + for (int x = 0; x <= gridSize.x; ++x) { + int screenX = mapToScreenX(x, gridSize.x, width); + for (int y = 0; y < height; ++y) { + int index = (y * width + screenX) * 3; + if (y % 5 == 0) { // Dotted line + pixels[index] = 255; // B + pixels[index + 1] = 255; // G + pixels[index + 2] = 255; // R + } + } + } + + for (int y = 0; y <= gridSize.y; ++y) { + int screenY = mapToScreenY(y, gridSize.y, height); + for (int x = 0; x < width; ++x) { + int index = (screenY * width + x) * 3; + if (x % 5 == 0) { // Dotted line + pixels[index] = 255; // B + pixels[index + 1] = 255; // G + pixels[index + 2] = 255; // R + } + } + } + } + + static void drawVoxel(std::vector& pixels, int width, int height, + const Vec3& voxel, const Vec3& gridSize, const Vec4& color) { + int screenX = mapToScreenX(voxel.x, gridSize.x, width); + int screenY = mapToScreenY(voxel.y, gridSize.y, height); + + uint8_t r, g, b; + color.toUint8(r, g, b); + + // Draw a 4x4 square for the voxel + for (int dy = -2; dy <= 2; ++dy) { + for (int dx = -2; dx <= 2; ++dx) { + int px = screenX + dx; + int py = screenY + dy; + if (px >= 0 && px < width && py >= 0 && py < height) { + int index = (py * width + px) * 3; + pixels[index] = b; + pixels[index + 1] = g; + pixels[index + 2] = r; + } + } + } + } + + static void drawRay(std::vector& pixels, int width, int height, + const AmanatidesWooAlgorithm::Ray& ray, const Vec3& gridSize) { + // Draw ray origin and direction + int originX = mapToScreenX(ray.origin.x, gridSize.x, width); + int originY = mapToScreenY(ray.origin.y, gridSize.y, height); + + // Draw ray origin (green) + for (int dy = -3; dy <= 3; ++dy) { + for (int dx = -3; dx <= 3; ++dx) { + int px = originX + dx; + int py = originY + dy; + if (px >= 0 && px < width && py >= 0 && py < height) { + int index = (py * width + px) * 3; + pixels[index] = 0; // B + pixels[index + 1] = 255; // G + pixels[index + 2] = 0; // R + } + } + } + + // Draw ray direction (yellow line) + Vec3 endPoint = ray.origin + ray.direction * 10.0f; // Extend ray + int endX = mapToScreenX(endPoint.x, gridSize.x, width); + int endY = mapToScreenY(endPoint.y, gridSize.y, height); + + drawLine(pixels, width, height, originX, originY, endX, endY, Vec4(1, 1, 0, 1)); + } + + static void drawLine(std::vector& pixels, int width, int height, + int x0, int y0, int x1, int y1, const Vec4& color) { + uint8_t r, g, b; + color.toUint8(r, g, b); + + int dx = std::abs(x1 - x0); + int dy = std::abs(y1 - y0); + int sx = (x0 < x1) ? 1 : -1; + int sy = (y0 < y1) ? 1 : -1; + int err = dx - dy; + + while (true) { + if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) { + int index = (y0 * width + x0) * 3; + pixels[index] = b; + pixels[index + 1] = g; + pixels[index + 2] = r; + } + + if (x0 == x1 && y0 == y1) break; + + int e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + } + + static int mapToScreenX(float x, float gridWidth, int screenWidth) { + return static_cast((x / gridWidth) * screenWidth); + } + + static int mapToScreenY(float y, float gridHeight, int screenHeight) { + return static_cast((y / gridHeight) * screenHeight); + } +}; + +// Noise generation functions +float fade(const float& a) { + TIME_FUNCTION; + return a * a * a * (10 + a * (-15 + a * 6)); +} + +float clamp(float x, float lowerlimit = 0.0f, float upperlimit = 1.0f) { + TIME_FUNCTION; + if (x < lowerlimit) return lowerlimit; + if (x > upperlimit) return upperlimit; + return x; +} + +float pascalTri(const float& a, const float& b) { + TIME_FUNCTION; + int result = 1; + for (int i = 0; i < b; ++i){ + result *= (a - 1) / (i + 1); + } + return result; +} + +float genSmooth(int N, float x) { + TIME_FUNCTION; + x = clamp(x, 0, 1); + float result = 0; + for (int n = 0; n <= N; ++n){ + result += pascalTri(-N - 1, n) * pascalTri(2 * N + 1, N-1) * pow(x, N + n + 1); + } + return result; +} + +float inverse_smoothstep(float x) { + TIME_FUNCTION; + return 0.5 - sin(asin(1.0 - 2.0 * x) / 3.0); +} + +float lerp(const float& t, const float& a, const float& b) { + TIME_FUNCTION; + return a + t * (b - a); +} + +float grad(const int& hash, const float& b, const float& c, const float& d) { + TIME_FUNCTION; + int h = hash & 15; + float u = (h < 8) ? c : b; + float v = (h < 4) ? b : ((h == 12 || h == 14) ? c : d); + return (((h & 1) == 0) ? u : -u) + (((h & 2) == 0) ? v : -v); +} + +float pnoise3d(const int p[512], const float& xf, const float& yf, const float& zf) { + TIME_FUNCTION; + int floorx = std::floor(xf); + int floory = std::floor(yf); + int floorz = std::floor(zf); + int iX = floorx & 255; + int iY = floory & 255; + int iZ = floorz & 255; + + float x = xf - floorx; + float y = yf - floory; + float z = zf - floorz; + + float u = fade(x); + float v = fade(y); + float w = fade(z); + + int A = p[iX] + iY; + int AA = p[A] + iZ; + int AB = p[A+1] + iZ; + + int B = p[iX + 1] + iY; + int BA = p[B] + iZ; + int BB = p[B+1] + iZ; + + float f = grad(p[BA], x-1, y, z); + float g = grad(p[AA], x, y, z); + float h = grad(p[BB], x-1, y-1, z); + float j = grad(p[BB], x-1, y-1, z); + float k = grad(p[AA+1], x, y, z-1); + float l = grad(p[BA+1], x-1, y, z-1); + float m = grad(p[AB+1], x, y-1, z-1); + float n = grad(p[BB+1], x-1, y-1, z-1); + + float o = lerp(u, m, n); + float q = lerp(u, k, l); + float r = lerp(u, h, j); + float s = lerp(u, f, g); + float t = lerp(v, q, o); + float e = lerp(v, s, r); + float d = lerp(w, e, t); + return d; +} + +std::tuple, std::vector> noiseBatch(int num_points, float scale, int sp[]) { + TIME_FUNCTION; + std::vector points; + std::vector colors; + points.reserve(num_points); + colors.reserve(num_points); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dis(-scale, scale); + + for (int i = 0; i < num_points; ++i) { + float x = dis(gen); + float y = dis(gen); + float z = dis(gen); + + float noise1 = pnoise3d(sp, (x * 0.5f), (y * 0.5f), (z * 0.5f)); + float noise2 = pnoise3d(sp, (x * 0.3f), (y * 0.3f), (z * 0.3f)); + float noise3 = pnoise3d(sp, (x * 0.7f), (y * 0.7f), (z * 0.7f)); + float noise4 = pnoise3d(sp, (x * 0.7f), (y * 0.7f), (z * 0.7f)); + + if (noise1 > 0.1f) { + float rt = (noise1 + 1.0f) * 0.5f; + float gt = (noise2 + 1.0f) * 0.5f; + float bt = (noise3 + 1.0f) * 0.5f; + float at = (noise4 + 1.0f) * 0.5f; + + float maxV = std::max({rt, gt, bt}); + if (maxV > 0) { + float r = rt / maxV; + float g = gt / maxV; + float b = bt / maxV; + float a = at / maxV; + points.push_back({x, y, z}); + colors.push_back({r, g, b, a}); + } + } + } + return std::make_tuple(points, colors); +} + +// Generate points in a more spherical distribution +std::tuple, std::vector> genPointCloud(int num_points, float radius, int seed) { + TIME_FUNCTION; + int permutation[256]; + for (int i = 0; i < 256; ++i) { + permutation[i] = i; + } + std::mt19937 rng(seed); + std::shuffle(permutation, permutation+256, rng); + int p[512]; + for (int i = 0; i < 256; ++i) { + p[i] = permutation[i]; + p[i + 256] = permutation[i]; + } + + std::vector points; + std::vector colors; + points.reserve(num_points); + colors.reserve(num_points); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dist(-1.0f, 1.0f); + std::uniform_real_distribution radius_dist(0.0f, radius); + + for (int i = 0; i < num_points; ++i) { + // Generate random direction + Vec3 direction; + do { + direction = Vec3(dist(gen), dist(gen), dist(gen)); + } while (direction.lengthSquared() == 0); + direction = direction.normalized(); + + // Generate random radius with some noise + float r = radius_dist(gen); + + Vec3 point = direction * r; + + // Use noise based on spherical coordinates for more natural distribution + float noise = pnoise3d(p, point.x * 0.5f, point.y * 0.5f, point.z * 0.5f); + + if (noise > 0.1f) { + // Color based on position and noise + float rt = (point.x / radius + 1.0f) * 0.5f; + float gt = (point.y / radius + 1.0f) * 0.5f; + float bt = (point.z / radius + 1.0f) * 0.5f; + float at = (noise + 1.0f) * 0.5f; + + points.push_back(point); + colors.push_back({rt, gt, bt, at}); + } + } + return std::make_tuple(points, colors); +} + +// Function to populate voxel grid with point cloud data +void populateVoxelGridWithPointCloud(VoxelGrid& grid, + const std::vector& points, + const std::vector& colors) { + TIME_FUNCTION; + printf("Populating voxel grid with %zu points...\n", points.size()); + + for (size_t i = 0; i < points.size(); i++) { + grid.addVoxel(points[i], colors[i]); + } + + printf("Voxel grid populated with %zu voxels\n", grid.getOccupiedPositions().size()); +} + +// Enhanced visualization function +void visualizePointCloud(const std::vector& points, const std::vector& colors, + const std::string& filename, int width = 800, int height = 600) { + TIME_FUNCTION; + std::vector pixels(width * height * 3, 0); + + // Background color (dark blue) + for (int i = 0; i < width * height * 3; i += 3) { + pixels[i] = 30; // B + pixels[i + 1] = 30; // G + pixels[i + 2] = 50; // R + } + + // Find bounds of point cloud + Vec3 minPoint( std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3 maxPoint(-std::numeric_limits::max(), -std::numeric_limits::max(), -std::numeric_limits::max()); + + for (const auto& point : points) { + minPoint.x = std::min(minPoint.x, point.x); + minPoint.y = std::min(minPoint.y, point.y); + minPoint.z = std::min(minPoint.z, point.z); + maxPoint.x = std::max(maxPoint.x, point.x); + maxPoint.y = std::max(maxPoint.y, point.y); + maxPoint.z = std::max(maxPoint.z, point.z); + } + + Vec3 cloudSize = maxPoint - minPoint; + float maxDim = std::max({cloudSize.x, cloudSize.y, cloudSize.z}); + + // Draw points + for (size_t i = 0; i < points.size(); i++) { + const auto& point = points[i]; + const auto& color = colors[i]; + + // Map 3D point to 2D screen coordinates (orthographic projection) + int screenX = static_cast(((point.x - minPoint.x) / maxDim) * (width - 20)) + 10; + int screenY = static_cast(((point.y - minPoint.y) / maxDim) * (height - 20)) + 10; + + if (screenX >= 0 && screenX < width && screenY >= 0 && screenY < height) { + uint8_t r, g, b; + color.toUint8(r, g, b); + + // Draw a 2x2 pixel for each point + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + int px = screenX + dx; + int py = screenY + dy; + if (px >= 0 && px < width && py >= 0 && py < height) { + int index = (py * width + px) * 3; + pixels[index] = b; + pixels[index + 1] = g; + pixels[index + 2] = r; + } + } + } + } + } + + BMPWriter::saveBMP(filename, pixels, width, height); +} + +// Replace your main function with this improved version: +int main() { + printf("=== Point Cloud Generation and Visualization ===\n\n"); + + // Generate point cloud using noise function + printf("Generating point cloud...\n"); + float cloudScale = 5.0f; + auto [points, colors] = genPointCloud(500000, cloudScale, 42); + printf("Generated %zu points\n\n", points.size()); + + // Calculate actual bounds of the point cloud + Vec3 minPoint(std::numeric_limits::max()); + Vec3 maxPoint(-std::numeric_limits::max()); + + for (const auto& point : points) { + minPoint.x = std::min(minPoint.x, point.x); + minPoint.y = std::min(minPoint.y, point.y); + minPoint.z = std::min(minPoint.z, point.z); + maxPoint.x = std::max(maxPoint.x, point.x); + maxPoint.y = std::max(maxPoint.y, point.y); + maxPoint.z = std::max(maxPoint.z, point.z); + } + + Vec3 cloudCenter = (minPoint + maxPoint) * 0.5f; + Vec3 cloudSize = maxPoint - minPoint; + + printf("Point cloud bounds:\n"); + printf(" Min: (%.2f, %.2f, %.2f)\n", minPoint.x, minPoint.y, minPoint.z); + printf(" Max: (%.2f, %.2f, %.2f)\n", maxPoint.x, maxPoint.y, maxPoint.z); + printf(" Center: (%.2f, %.2f, %.2f)\n", cloudCenter.x, cloudCenter.y, cloudCenter.z); + printf(" Size: (%.2f, %.2f, %.2f)\n", cloudSize.x, cloudSize.y, cloudSize.z); + + // Create a voxel grid that properly contains the point cloud + // Add some padding around the cloud + float padding = 2.0f; + Vec3 gridWorldSize = cloudSize + Vec3(padding * 2); + Vec3 gridWorldMin = cloudCenter - gridWorldSize * 0.5f; + + // Use smaller voxels for better resolution + Vec3 voxelSize(0.1f, 0.1f, 0.1f); + Vec3 gridSize = (gridWorldSize / voxelSize).ceil(); + + printf("\nVoxel grid configuration:\n"); + printf(" World size: (%.2f, %.2f, %.2f)\n", gridWorldSize.x, gridWorldSize.y, gridWorldSize.z); + printf(" Grid dimensions: (%d, %d, %d)\n", (int)gridSize.x, (int)gridSize.y, (int)gridSize.z); + printf(" Voxel size: (%.2f, %.2f, %.2f)\n", voxelSize.x, voxelSize.y, voxelSize.z); + + VoxelGrid grid(gridSize, voxelSize); + + // Center the point cloud in the voxel grid + printf("\nPopulating voxel grid...\n"); + size_t voxelsAdded = 0; + for (size_t i = 0; i < points.size(); i++) { + // Use the original point positions - the grid will handle world-to-grid conversion + grid.addVoxel(points[i], colors[i]); + voxelsAdded++; + } + + printf("Voxel grid populated with %zu voxels (out of %zu points)\n", + grid.getOccupiedPositions().size(), points.size()); + + // Test if the cloud is properly centered by checking voxel distribution + auto& occupied = grid.getOccupiedPositions(); + Vec3 gridMin(std::numeric_limits::max()); + Vec3 gridMax(-std::numeric_limits::max()); + + for (const auto& pos : occupied) { + gridMin.x = std::min(gridMin.x, pos.x); + gridMin.y = std::min(gridMin.y, pos.y); + gridMin.z = std::min(gridMin.z, pos.z); + gridMax.x = std::max(gridMax.x, pos.x); + gridMax.y = std::max(gridMax.y, pos.y); + gridMax.z = std::max(gridMax.z, pos.z); + } + + printf("\nVoxel distribution in grid:\n"); + printf(" Grid min: (%.2f, %.2f, %.2f)\n", gridMin.x, gridMin.y, gridMin.z); + printf(" Grid max: (%.2f, %.2f, %.2f)\n", gridMax.x, gridMax.y, gridMax.z); + printf(" Grid center: (%.2f, %.2f, %.2f)\n", + (gridMin.x + gridMax.x) * 0.5f, + (gridMin.y + gridMax.y) * 0.5f, + (gridMin.z + gridMax.z) * 0.5f); + + // Visualizations + printf("\nCreating visualizations...\n"); + visualizePointCloud(points, colors, "point_cloud_visualization.bmp"); + printf("Saved point cloud visualization to 'point_cloud_visualization.bmp'\n"); + + // Save slices at different heights through the cloud + int centerZ = static_cast(gridSize.z * 0.5f); + for (int offset = -2; offset <= 2; offset++) { + int sliceZ = centerZ + offset; + std::string filename = "voxel_slice_z" + std::to_string(offset) + ".bmp"; + if (BMPWriter::saveVoxelGridSlice(filename, grid, sliceZ)) { + printf("Saved voxel grid slice to '%s'\n", filename.c_str()); + } + } + + // Test ray tracing through the center of the cloud + printf("\n=== Ray Tracing Test ===\n"); + + // Create rays that go through the center of the cloud from different directions + std::vector testRays = { + AmanatidesWooAlgorithm::Ray(cloudCenter - Vec3(10, 0, 0), Vec3(1, 0, 0), 20.0f), // X direction + AmanatidesWooAlgorithm::Ray(cloudCenter - Vec3(0, 10, 0), Vec3(0, 1, 0), 20.0f), // Y direction + AmanatidesWooAlgorithm::Ray(cloudCenter - Vec3(0, 0, 10), Vec3(0, 0, 1), 20.0f), // Z direction + AmanatidesWooAlgorithm::Ray(cloudCenter - Vec3(8, 8, 0), Vec3(1, 1, 0).normalized(), 20.0f) // Diagonal + }; + + for (size_t i = 0; i < testRays.size(); i++) { + std::vector hitVoxels; + std::vector hitDistances; + + bool hit = AmanatidesWooAlgorithm::traverse(testRays[i], grid, hitVoxels, hitDistances); + + printf("Ray %zu: %s (%zu hits)\n", i, hit ? "HIT" : "MISS", hitVoxels.size()); + + // Save visualization for this ray + std::string rayFilename = "ray_trace_" + std::to_string(i) + ".bmp"; + BMPWriter::saveRayTraceResults(rayFilename, grid, hitVoxels, testRays[i]); + printf(" Saved ray trace to '%s'\n", rayFilename.c_str()); + } + + printf("\n=== Statistics ===\n"); + printf("Total points generated: %zu\n", points.size()); + printf("Voxels in grid: %zu\n", grid.getOccupiedPositions().size()); + printf("Grid size: (%.1f, %.1f, %.1f)\n", gridSize.x, gridSize.y, gridSize.z); + printf("Voxel size: (%.1f, %.1f, %.1f)\n", voxelSize.x, voxelSize.y, voxelSize.z); + + FunctionTimer::printStats(FunctionTimer::Mode::ENHANCED); + + return 0; +} \ No newline at end of file diff --git a/f.cpp b/f.cpp new file mode 100644 index 0000000..3395af3 --- /dev/null +++ b/f.cpp @@ -0,0 +1,363 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "util/timing_decorator.hpp" +#include "util/vec3.hpp" +#include "util/vec4.hpp" +#include "util/bmpwriter.hpp" +#include "util/voxelgrid.hpp" + +std::vector generateSphere(int numPoints, float radius = 1.0f, float wiggleAmount = 0.1f) { + + printf("Generating sphere with %d points using grid method...\n", numPoints); + printf("Wiggle amount: %.3f\n", wiggleAmount); + + std::vector points; + + // Random number generator for wiggling + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dist(-1.0f, 1.0f); + + // Calculate grid resolution based on desired number of points + // For a sphere, we need to account that only about 52% of points in a cube will be inside the sphere + int gridRes = static_cast(std::cbrt(numPoints / 0.52f)) + 1; + + printf("Using grid resolution: %d x %d x %d\n", gridRes, gridRes, gridRes); + + // Calculate voxel size for wiggling (based on average distance between points) + float voxelSize = 2.0f / gridRes; + float maxWiggle = wiggleAmount * voxelSize; + + printf("Voxel size: %.4f, Max wiggle: %.4f\n", voxelSize, maxWiggle); + + // Generate points in a cube from -1 to 1 in all dimensions + for (int x = 0; x < gridRes; ++x) { + for (int y = 0; y < gridRes; ++y) { + for (int z = 0; z < gridRes; ++z) { + // Convert grid coordinates to normalized cube coordinates [-1, 1] + Vec3 point( + (2.0f * x / (gridRes - 1)) - 1.0f, + (2.0f * y / (gridRes - 1)) - 1.0f, + (2.0f * z / (gridRes - 1)) - 1.0f + ); + + // Check if point is inside the unit sphere + if (point.lengthSquared() <= 1.0f) { + // Apply randomized wiggling + Vec3 wiggle( + dist(gen) * maxWiggle, + dist(gen) * maxWiggle, + dist(gen) * maxWiggle + ); + + Vec3 wiggledPoint = point + wiggle; + + // Re-normalize to maintain spherical shape while preserving the wiggle + // This ensures the point stays within the sphere while having natural variation + float currentLength = wiggledPoint.length(); + if (currentLength > 1.0f) { + // Scale back to unit sphere surface, but preserve the wiggle direction + wiggledPoint = wiggledPoint * (1.0f / currentLength); + } + + points.push_back(wiggledPoint * radius); // Scale by radius + } + } + } + } + + printf("Generated %zu points inside sphere\n", points.size()); + + // If we have too many points, randomly sample down to the desired number + if (points.size() > static_cast(numPoints)) { + printf("Sampling down from %zu to %d points...\n", points.size(), numPoints); + + // Shuffle and resize + std::shuffle(points.begin(), points.end(), gen); + points.resize(numPoints); + } + // If we have too few points, we'll use what we have + else if (points.size() < static_cast(numPoints)) { + printf("Warning: Only generated %zu points (requested %d)\n", points.size(), numPoints); + } + + return points; +} + +// Alternative sphere generation with perlin-like noise for more natural wiggling +std::vector generateSphereWithNaturalWiggle(int numPoints, float radius = 1.0f, float noiseStrength = 0.15f) { + + printf("Generating sphere with natural wiggling using %d points...\n", numPoints); + printf("Noise strength: %.3f\n", noiseStrength); + + std::vector points; + + // Random number generators + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dist(-1.0f, 1.0f); + + // Calculate grid resolution + int gridRes = static_cast(std::cbrt(numPoints / 0.52f)) + 1; + printf("Using grid resolution: %d x %d x %d\n", gridRes, gridRes, gridRes); + + float voxelSize = 2.0f / gridRes; + float maxDisplacement = noiseStrength * voxelSize; + + // Pre-compute some random offsets for pseudo-perlin noise + std::vector randomOffsets(gridRes * gridRes * gridRes); + for (size_t i = 0; i < randomOffsets.size(); ++i) { + randomOffsets[i] = dist(gen); + } + + auto getNoise = [&](int x, int y, int z) -> float { + int idx = (x * gridRes * gridRes) + (y * gridRes) + z; + return randomOffsets[idx % randomOffsets.size()]; + }; + + for (int x = 0; x < gridRes; ++x) { + for (int y = 0; y < gridRes; ++y) { + for (int z = 0; z < gridRes; ++z) { + Vec3 point( + (2.0f * x / (gridRes - 1)) - 1.0f, + (2.0f * y / (gridRes - 1)) - 1.0f, + (2.0f * z / (gridRes - 1)) - 1.0f + ); + + if (point.lengthSquared() <= 1.0f) { + // Use smoother noise for more natural displacement + float noiseX = getNoise(x, y, z); + float noiseY = getNoise(y, z, x); + float noiseZ = getNoise(z, x, y); + + // Apply displacement that preserves the overall spherical structure + Vec3 displacement( + noiseX * maxDisplacement, + noiseY * maxDisplacement, + noiseZ * maxDisplacement + ); + + Vec3 displacedPoint = point + displacement; + + // Ensure we don't push points too far from sphere surface + float currentLength = displacedPoint.length(); + if (currentLength > 1.0f) { + displacedPoint = displacedPoint * (0.95f + 0.05f * dist(gen)); // Small random scaling + } + + points.push_back(displacedPoint * radius); + } + } + } + } + + printf("Generated %zu points with natural wiggling\n", points.size()); + + // Sample down if needed + if (points.size() > static_cast(numPoints)) { + printf("Sampling down from %zu to %d points...\n", points.size(), numPoints); + std::shuffle(points.begin(), points.end(), gen); + points.resize(numPoints); + } else if (points.size() < static_cast(numPoints)) { + printf("Warning: Only generated %zu points (requested %d)\n", points.size(), numPoints); + } + + return points; +} + + +// Modified function to populate voxel grid with sphere points and assign layers +void populateVoxelGridWithLayeredSphere(VoxelGrid& grid, const std::vector& points) { + printf("Populating voxel grid with %zu sphere points...\n", points.size()); + + // First add all voxels with a default color + Vec4 defaultColor(1.0f, 1.0f, 1.0f, 1.0f); // Temporary color + for (const auto& point : points) { + grid.addVoxel(point, defaultColor); + } + + printf("Voxel grid populated with %zu voxels\n", grid.getOccupiedPositions().size()); + + // Now assign the planetary layers + grid.assignPlanetaryLayers(); +} + +Vec3 rotateX(const Vec3& point, float angle) { + float cosA = std::cos(angle); + float sinA = std::sin(angle); + return Vec3( + point.x, + point.y * cosA - point.z * sinA, + point.y * sinA + point.z * cosA + ); +} + +Vec3 rotateY(const Vec3& point, float angle) { + float cosA = std::cos(angle); + float sinA = std::sin(angle); + return Vec3( + point.x * cosA + point.z * sinA, + point.y, + -point.x * sinA + point.z * cosA + ); +} + +Vec3 rotateZ(const Vec3& point, float angle) { + float cosA = std::cos(angle); + float sinA = std::sin(angle); + return Vec3( + point.x * cosA - point.y * sinA, + point.x * sinA + point.y * cosA, + point.z + ); +} + +void visualizePointCloud(const std::vector& points, const std::vector& colors, + const std::string& filename, int width = 1000, int height = 1000, + float angleX = 0.0f, float angleY = 0.0f, float angleZ = 0.0f) { + TIME_FUNCTION; + std::vector pixels(width * height * 3, 0); + + // Background color (dark blue) + for (int i = 0; i < width * height * 3; i += 3) { + pixels[i] = 30; // B + pixels[i + 1] = 30; // G + pixels[i + 2] = 50; // R + } + + // Find bounds of point cloud + Vec3 minPoint( std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); + Vec3 maxPoint(-std::numeric_limits::max(), -std::numeric_limits::max(), -std::numeric_limits::max()); + + // Apply rotation to all points and find new bounds + std::vector rotatedPoints; + rotatedPoints.reserve(points.size()); + + for (const auto& point : points) { + Vec3 rotated = point; + rotated = rotateX(rotated, angleX); + rotated = rotateY(rotated, angleY); + rotated = rotateZ(rotated, angleZ); + rotatedPoints.push_back(rotated); + + minPoint.x = std::min(minPoint.x, rotated.x); + minPoint.y = std::min(minPoint.y, rotated.y); + minPoint.z = std::min(minPoint.z, rotated.z); + maxPoint.x = std::max(maxPoint.x, rotated.x); + maxPoint.y = std::max(maxPoint.y, rotated.y); + maxPoint.z = std::max(maxPoint.z, rotated.z); + } + + Vec3 cloudSize = maxPoint - minPoint; + float maxDim = std::max({cloudSize.x, cloudSize.y, cloudSize.z}); + + // Draw points + for (size_t i = 0; i < rotatedPoints.size(); i++) { + const auto& point = rotatedPoints[i]; + const auto& color = colors[i]; + + // Map 3D point to 2D screen coordinates (orthographic projection) + int screenX = static_cast(((point.x - minPoint.x) / maxDim) * (width - 20)) + 10; + int screenY = static_cast(((point.y - minPoint.y) / maxDim) * (height - 20)) + 10; + + if (screenX >= 0 && screenX < width && screenY >= 0 && screenY < height) { + uint8_t r, g, b; + color.toUint8(r, g, b); + + // Draw a 2x2 pixel for each point + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + int px = screenX + dx; + int py = screenY + dy; + if (px >= 0 && px < width && py >= 0 && py < height) { + int index = (py * width + px) * 3; + pixels[index] = b; + pixels[index + 1] = g; + pixels[index + 2] = r; + } + } + } + } + } + + BMPWriter::saveBMP(filename, pixels, width, height); +} + +int main() { + printf("=== Layered Sphere Generation and Visualization ===\n\n"); + + const int numPoints = 100000000; + const float radius = 2.0f; + + printf("Generating layered spheres with %d points each, radius %.1f\n\n", numPoints, radius); + + // Create voxel grid + VoxelGrid grid(Vec3(10, 10, 10), Vec3(0.1f, 0.1f, 0.1f)); + + // Generate sphere with different wiggle options: + + // Option 1: Simple random wiggling (small amount - 10% of voxel size) + printf("1. Generating sphere with simple wiggling...\n"); + auto sphere1 = generateSphereWithNaturalWiggle(numPoints, radius, 0.1f); + + populateVoxelGridWithLayeredSphere(grid, sphere1); + + // Extract positions and colors for visualization + std::vector occupiedPositions = grid.getOccupiedPositions(); + std::vector layerColors = grid.getColors(); + + // Convert to world coordinates for visualization + std::vector worldPositions; + for (const auto& gridPos : occupiedPositions) { + worldPositions.push_back(grid.gridToWorld(gridPos)); + } + + // Create multiple visualizations from different angles + printf("\nGenerating views from different angles...\n"); + + // Front view (0, 0, 0) + visualizePointCloud(worldPositions, layerColors, "output/sphere_front.bmp", 1000, 1000, 0.0f, 0.0f, 0.0f); + printf(" - sphere_front.bmp (front view)\n"); + + // 45 degree rotation around Y axis + visualizePointCloud(worldPositions, layerColors, "output/sphere_45y.bmp", 1000, 1000, 0.0f, M_PI/4, 0.0f); + printf(" - sphere_45y.bmp (45° Y rotation)\n"); + + // 90 degree rotation around Y axis (side view) + visualizePointCloud(worldPositions, layerColors, "output/sphere_side.bmp", 1000, 1000, 0.0f, M_PI/2, 0.0f); + printf(" - sphere_side.bmp (side view)\n"); + + // 45 degree rotation around X axis (top-down perspective) + visualizePointCloud(worldPositions, layerColors, "output/sphere_45x.bmp", 1000, 1000, M_PI/4, 0.0f, 0.0f); + printf(" - sphere_45x.bmp (45° X rotation)\n"); + + // Combined rotation (30° X, 30° Y) + visualizePointCloud(worldPositions, layerColors, "output/sphere_30x_30y.bmp", 1000, 1000, M_PI/6, M_PI/6, 0.0f); + printf(" - sphere_30x_30y.bmp (30° X, 30° Y rotation)\n"); + + // Top view (90° X rotation) + visualizePointCloud(worldPositions, layerColors, "output/sphere_top.bmp", 1000, 1000, M_PI/2, 0.0f, 0.0f); + printf(" - sphere_top.bmp (top view)\n"); + + printf("\n=== Sphere generated successfully ===\n"); + printf("Files created in output/ directory:\n"); + printf(" - sphere_front.bmp (Front view)\n"); + printf(" - sphere_45y.bmp (45° Y rotation)\n"); + printf(" - sphere_side.bmp (Side view)\n"); + printf(" - sphere_45x.bmp (45° X rotation)\n"); + printf(" - sphere_30x_30y.bmp (30° X, 30° Y rotation)\n"); + printf(" - sphere_top.bmp (Top view)\n"); + + return 0; +} \ No newline at end of file diff --git a/g.cpp b/g.cpp new file mode 100644 index 0000000..fd6f221 --- /dev/null +++ b/g.cpp @@ -0,0 +1,17 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "timing_decorator.hpp" +#include "util/vec3.hpp" +#include "util/vec4.hpp" +#include "util/bmpwriter.hpp" diff --git a/gradient.bmp b/gradient.bmp new file mode 100644 index 0000000000000000000000000000000000000000..dca969a66a27b8c516fe9870b370e39a245b4242 GIT binary patch literal 786486 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 Ora)7mDe(VQ;C}&DjKanM literal 0 HcmV?d00001 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..12fb964 --- /dev/null +++ b/main.cpp @@ -0,0 +1,136 @@ +#include +#include +#include +#include "util/grid2.hpp" +#include "util/bmpwriter.hpp" + +// Function to convert hex color string to Vec4 +Vec4 hexToVec4(const std::string& hex) { + if (hex.length() != 6) { + return Vec4(0, 0, 0, 1); // Default to black if invalid + } + + int r, g, b; + sscanf(hex.c_str(), "%02x%02x%02x", &r, &g, &b); + + return Vec4(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f); +} + +int main(int argc, char* argv[]) { + // Check for gradient flag + bool createGradient = false; + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--gradient" || arg == "-g") { + createGradient = true; + break; + } + } + + if (!createGradient) { + std::cout << "Usage: " << argv[0] << " --gradient (-g)" << std::endl; + std::cout << "Creates a gradient image with red, green, and blue corners" << std::endl; + return 1; + } + + // Create a grid with points arranged in a gradient pattern + const int WIDTH = 512; + const int HEIGHT = 512; + const int POINTS_PER_DIM = 256; // Resolution of the gradient + + Grid2 grid; + + // Define our target colors at specific positions + Vec4 red = hexToVec4("ff0000"); // Top-left corner + Vec4 green = hexToVec4("00ff00"); // Center + Vec4 blue = hexToVec4("0000ff"); // Bottom-right corner + Vec4 white = hexToVec4("ffffff"); // Top-right corner + Vec4 black = hexToVec4("000000"); // Bottom-left corner + + // Create gradient points + for (int y = 0; y < POINTS_PER_DIM; ++y) { + for (int x = 0; x < POINTS_PER_DIM; ++x) { + // Normalize coordinates to [0, 1] + float nx = static_cast(x) / (POINTS_PER_DIM - 1); + float ny = static_cast(y) / (POINTS_PER_DIM - 1); + + // Create position in [-1, 1] range + Vec2 pos(nx * 2.0f - 1.0f, ny * 2.0f - 1.0f); + + // Calculate interpolated color based on position + Vec4 color; + + if (nx + ny <= 1.0f) { + // Lower triangle: interpolate between red, green, and black + if (nx <= 0.5f && ny <= 0.5f) { + // Bottom-left quadrant: red to black to green + float t1 = nx * 2.0f; // Horizontal interpolation + float t2 = ny * 2.0f; // Vertical interpolation + + if (t1 + t2 <= 1.0f) { + // Interpolate between red and black + color = red * (1.0f - t1 - t2) + black * (t1 + t2); + } else { + // Interpolate between black and green + color = black * (2.0f - t1 - t2) + green * (t1 + t2 - 1.0f); + } + } else { + // Use bilinear interpolation for other areas + Vec4 topLeft = red; + Vec4 topRight = white; + Vec4 bottomLeft = black; + Vec4 bottomRight = green; + + Vec4 top = topLeft * (1.0f - nx) + topRight * nx; + Vec4 bottom = bottomLeft * (1.0f - nx) + bottomRight * nx; + color = bottom * (1.0f - ny) + top * ny; + } + } else { + // Upper triangle: interpolate between green, blue, and white + if (nx >= 0.5f && ny >= 0.5f) { + // Top-right quadrant: green to white to blue + float t1 = (nx - 0.5f) * 2.0f; // Horizontal interpolation + float t2 = (ny - 0.5f) * 2.0f; // Vertical interpolation + + if (t1 + t2 <= 1.0f) { + // Interpolate between green and white + color = green * (1.0f - t1 - t2) + white * (t1 + t2); + } else { + // Interpolate between white and blue + color = white * (2.0f - t1 - t2) + blue * (t1 + t2 - 1.0f); + } + } else { + // Use bilinear interpolation for other areas + Vec4 topLeft = red; + Vec4 topRight = white; + Vec4 bottomLeft = black; + Vec4 bottomRight = blue; + + Vec4 top = topLeft * (1.0f - nx) + topRight * nx; + Vec4 bottom = bottomLeft * (1.0f - nx) + bottomRight * nx; + color = bottom * (1.0f - ny) + top * ny; + } + } + + grid.addPoint(pos, color); + } + } + + // Render to RGB image + std::vector imageData = grid.renderToRGB(WIDTH, HEIGHT); + + // Save as BMP + if (BMPWriter::saveBMP("output/gradient.bmp", imageData, WIDTH, HEIGHT)) { + std::cout << "Gradient image saved as 'gradient.bmp'" << std::endl; + std::cout << "Colors: " << std::endl; + std::cout << " Top-left: ff0000 (red)" << std::endl; + std::cout << " Center: 00ff00 (green)" << std::endl; + std::cout << " Bottom-right: 0000ff (blue)" << std::endl; + std::cout << " Gradient between ffffff and 000000 throughout" << std::endl; + } else { + std::cerr << "Failed to save gradient image" << std::endl; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/pointcloud_renderer b/pointcloud_renderer new file mode 100644 index 0000000000000000000000000000000000000000..86889f42e3008b44fbbd9527732f75ffad61e2a3 GIT binary patch literal 48416 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

1b_cU`X21q3Cb06>**86$X1 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 literal 0 HcmV?d00001 diff --git a/sim.cpp b/sim.cpp new file mode 100644 index 0000000..a591532 --- /dev/null +++ b/sim.cpp @@ -0,0 +1,479 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#pragma comment(lib, "ws2_32.lib") +#else +#include +#include +#include +#include +#endif + +#define M_PI 3.14159265358979323846 +#define M_PHI 1.61803398874989484820458683436563811772030917980576286213544862270526046281890 +#define M_TAU 6.2831853071795864769252867665590057683943387987502116419498891846156328125724179972560696506842341359 + +class Vec3 { +public: + double x, y, z; + + Vec3(double x = 0, double y = 0, double z = 0) : x(x), y(y), z(z) {} + + inline double norm() const { + return std::sqrt(x*x + y*y + z*z); + } + + inline Vec3 normalize() const { + double n = norm(); + return Vec3(x/n, y/n, z/n); + } + + inline Vec3 cross(const Vec3& other) const { + return Vec3( + y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x + ); + } + + inline double dot(const Vec3& other) const { + return x * other.x + y * other.y + z * other.z; + } + + inline Vec3 operator+(const Vec3& other) const { + return Vec3(x + other.x, y + other.y, z + other.z); + } + + inline Vec3 operator-(const Vec3& other) const { + return Vec3(x - other.x, y - other.y, z - other.z); + } + + inline Vec3 operator*(double scalar) const { + return Vec3(x * scalar, y * scalar, z * scalar); + } + + inline Vec3 operator/(double scalar) const { + return Vec3(x / scalar, y / scalar, z / scalar); + } + + bool operator==(const Vec3& other) const { + return x == other.x && y == other.y && z == other.z; + } + + bool operator<(const Vec3& other) const { + if (x != other.x) return x < other.x; + if (y != other.y) return y < other.y; + return z < other.z; + } + + struct Hash { + size_t operator()(const Vec3& v) const { + size_t h1 = std::hash()(std::round(v.x * 1000.0)); + size_t h2 = std::hash()(std::round(v.y * 1000.0)); + size_t h3 = std::hash()(std::round(v.z * 1000.0)); + return h1 ^ (h2 << 1) ^ (h3 << 2); + } + }; +}; + +class Triangle { +public: + Vec3 v0, v1, v2; + + Triangle(const Vec3& v0, const Vec3& v1, const Vec3& v2) : v0(v0), v1(v1), v2(v2) {} + + Vec3 normal() const { + Vec3 edge1 = v1 - v0; + Vec3 edge2 = v2 - v0; + return edge1.cross(edge2).normalize(); + } +}; + +std::vector fibsphere(int numPoints, float radius) { + std::vector points; + points.reserve(numPoints); + + for (int i = 0; i < numPoints; ++i) { + double y = 1.0 - (i / (double)(numPoints - 1)) * 2.0; + double radius_at_y = std::sqrt(1.0 - y * y); + + double theta = 2.0 * M_PI * i / M_PHI; + + double x = std::cos(theta) * radius_at_y; + double z = std::sin(theta) * radius_at_y; + + points.emplace_back(x * radius, y * radius, z * radius); + } + + return points; +} + +// Create proper triangulation for Fibonacci sphere +std::vector createFibonacciSphereMesh(const std::vector& points) { + std::vector triangles; + int n = points.size(); + + // Create a map to quickly find points by their spherical coordinates + std::map, int> pointMap; + for (int i = 0; i < n; i++) { + double phi = std::acos(points[i].y / 3.0); // theta = acos(y/R) + double theta = std::atan2(points[i].z, points[i].x); + if (theta < 0) theta += 2.0 * M_PI; + pointMap[{phi, theta}] = i; + } + + // Create triangles by connecting neighboring points + for (int i = 0; i < n - 1; i++) { + // For each point, connect it to its neighbors + if (i > 0) { + triangles.emplace_back(points[i-1], points[i], points[(i+1) % n]); + } + } + + // Add cap triangles + for (int i = 1; i < n/2 - 1; i++) { + triangles.emplace_back(points[0], points[i], points[i+1]); + triangles.emplace_back(points[n-1], points[n-1-i], points[n-2-i]); + } + + return triangles; +} + +// Alternative: Use Delaunay triangulation on the sphere (simplified) +std::vector createSphereMeshDelaunay(const std::vector& points) { + std::vector triangles; + int n = points.size(); + + // Simple approach: connect each point to its nearest neighbors + // This is a simplified version - for production use a proper Delaunay triangulation + + for (int i = 0; i < n; i++) { + // Find nearest neighbors (simplified) + std::vector> distances; + for (int j = 0; j < n; j++) { + if (i != j) { + double dist = (points[i] - points[j]).norm(); + distances.emplace_back(dist, j); + } + } + + // Sort by distance and take 6 nearest neighbors + std::sort(distances.begin(), distances.end()); + int numNeighbors = min(6, (int)distances.size()); + + // Create triangles with nearest neighbors + for (int k = 0; k < numNeighbors - 1; k++) { + triangles.emplace_back(points[i], points[distances[k].second], points[distances[k+1].second]); + } + } + + return triangles; +} + +Vec3 rotate(const Vec3& point, double angleX, double angleY, double angleZ) { + // Rotate around X axis + double y1 = point.y * cos(angleX) - point.z * sin(angleX); + double z1 = point.y * sin(angleX) + point.z * cos(angleX); + + // Rotate around Y axis + double x2 = point.x * cos(angleY) + z1 * sin(angleY); + double z2 = -point.x * sin(angleY) + z1 * cos(angleY); + + // Rotate around Z axis + double x3 = x2 * cos(angleZ) - y1 * sin(angleZ); + double y3 = x2 * sin(angleZ) + y1 * cos(angleZ); + + return Vec3(x3, y3, z2); +} + +std::string generateSVG(const std::vector& points, const std::vector& mesh, + double angleX, double angleY, double angleZ) { + std::stringstream svg; + int width = 800; + int height = 600; + + svg << "\n"; + svg << "\n"; + svg << "\n"; + + // Project 3D to 2D + auto project = [&](const Vec3& point) -> std::pair { + Vec3 rotated = rotate(point, angleX, angleY, angleZ); + // Perspective projection + double scale = 300.0 / (5.0 + rotated.z); + double x = width / 2 + rotated.x * scale; + double y = height / 2 + rotated.y * scale; + return {x, y}; + }; + + // Draw triangles with shading + for (const auto& triangle : mesh) { + Vec3 normal = triangle.normal(); + Vec3 lightDir = Vec3(0.5, 0.7, 1.0).normalize(); + double intensity = max(0.0, normal.dot(lightDir)); + + // Calculate color based on intensity + int r = static_cast(50 + intensity * 200); + int g = static_cast(100 + intensity * 150); + int b = static_cast(200 + intensity * 55); + + auto [x0, y0] = project(triangle.v0); + auto [x1, y1] = project(triangle.v1); + auto [x2, y2] = project(triangle.v2); + + // Only draw triangles facing the camera + Vec3 viewDir(0, 0, 1); + if (normal.dot(viewDir) > 0.1) { + svg << "\n"; + } + } + + // Draw points for debugging + for (const auto& point : points) { + auto [x, y] = project(point); + svg << "\n"; + } + + svg << ""; + return svg.str(); +} + +// HTTP server class (keep your existing server implementation) +class SimpleHTTPServer { +private: + int serverSocket; + int port; + +public: + SimpleHTTPServer(int port) : port(port), serverSocket(-1) {} + + ~SimpleHTTPServer() { + stop(); + } + + bool start() { +#ifdef _WIN32 + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { + std::cerr << "WSAStartup failed" << std::endl; + return false; + } +#endif + + serverSocket = socket(AF_INET, SOCK_STREAM, 0); + if (serverSocket < 0) { + std::cerr << "Socket creation failed" << std::endl; + return false; + } + + int opt = 1; +#ifdef _WIN32 + if (setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)) < 0) { +#else + if (setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { +#endif + std::cerr << "Setsockopt failed" << std::endl; + return false; + } + + sockaddr_in serverAddr; + serverAddr.sin_family = AF_INET; + serverAddr.sin_addr.s_addr = INADDR_ANY; + serverAddr.sin_port = htons(port); + + if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) { + std::cerr << "Bind failed" << std::endl; + return false; + } + + if (listen(serverSocket, 10) < 0) { + std::cerr << "Listen failed" << std::endl; + return false; + } + + std::cout << "Server started on port " << port << std::endl; + return true; + } + + void stop() { + if (serverSocket >= 0) { +#ifdef _WIN32 + closesocket(serverSocket); + WSACleanup(); +#else + close(serverSocket); +#endif + serverSocket = -1; + } + } + + void handleRequests() { + // Generate proper Fibonacci sphere + std::vector spherePoints = fibsphere(200, 3.0); // Reduced for performance + std::vector sphereMesh = createSphereMeshDelaunay(spherePoints); + + while (true) { + sockaddr_in clientAddr; +#ifdef _WIN32 + int clientAddrLen = sizeof(clientAddr); +#else + socklen_t clientAddrLen = sizeof(clientAddr); +#endif + int clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &clientAddrLen); + + if (clientSocket < 0) { + std::cerr << "Accept failed" << std::endl; + continue; + } + + char buffer[4096] = {0}; + recv(clientSocket, buffer, sizeof(buffer), 0); + + std::string request(buffer); + std::string response; + + if (request.find("GET / ") != std::string::npos || request.find("GET /index.html") != std::string::npos) { + response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + getHTML(); + } else if (request.find("GET /mesh.svg") != std::string::npos) { + static double angle = 0.0; + angle += 0.02; + std::string svg = generateSVG(sphereMesh, angle, angle * 0.7, angle * 0.3); + response = "HTTP/1.1 200 OK\r\nContent-Type: image/svg+xml\r\n\r\n" + svg; + } else { + response = "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n\r\n404 Not Found"; + } + + send(clientSocket, response.c_str(), response.length(), 0); + +#ifdef _WIN32 + closesocket(clientSocket); +#else + close(clientSocket); +#endif + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + + std::string getHTML() { + return R"( + + + + 3D Sphere Mesh Renderer + + + +

+

3D Sphere Mesh Renderer

+ +
+ 3D Sphere +
+ <--- DO NOT EDIT ---> + +
+ + + + +)"; + } +}; + +int main() { + SimpleHTTPServer server(5101); + + if (!server.start()) { + std::cerr << "Failed to start server" << std::endl; + return 1; + } + + std::cout << "Open your browser and navigate to http://localhost:5101" << std::endl; + std::cout << "Press Ctrl+C to stop the server" << std::endl; + + server.handleRequests(); + + return 0; +} \ No newline at end of file diff --git a/util/Vec2.hpp b/util/Vec2.hpp new file mode 100644 index 0000000..35c05a2 --- /dev/null +++ b/util/Vec2.hpp @@ -0,0 +1,286 @@ +#ifndef VEC2_HPP +#define VEC2_HPP + +#include +#include +#include + +class Vec2 { + public: + float x, y; + Vec2() : x(0), y(0) {} + Vec2(float x, float y) : x(x), y(y) {} + + Vec2 operator+(const Vec2& other) const { + return Vec2(x + other.x, y + other.y); + } + + Vec2 operator-(const Vec2& other) const { + return Vec2(x - other.x, y - other.y); + } + + Vec2 operator*(const Vec2& other) const { + return Vec2(x * other.x, y * other.y); + } + + Vec2 operator/(const Vec2& other) const { + return Vec2(x / other.x, y / other.y); + } + + Vec2 operator+(float scalar) const { + return Vec2(x + scalar, y + scalar); + } + + Vec2 operator-(float scalar) const { + return Vec2(x - scalar, y - scalar); + } + + Vec2 operator-() const { + return Vec2(-x, -y); + } + + Vec2 operator*(float scalar) const { + return Vec2(x * scalar, y * scalar); + } + + Vec2 operator/(float scalar) const { + return Vec2(x / scalar, y / scalar); + } + + Vec2& operator=(float scalar) { + x = y = scalar; + return *this; + } + + Vec2& operator+=(const Vec2& other) { + x += other.x; + y += other.y; + return *this; + } + + Vec2& operator-=(const Vec2& other) { + x -= other.x; + y -= other.y; + return *this; + } + + Vec2& operator*=(const Vec2& other) { + x *= other.x; + y *= other.y; + return *this; + } + + Vec2& operator/=(const Vec2& other) { + x /= other.x; + y /= other.y; + return *this; + } + + Vec2& operator+=(float scalar) { + x += scalar; + y += scalar; + return *this; + } + + Vec2& operator-=(float scalar) { + x -= scalar; + y -= scalar; + return *this; + } + + Vec2& operator*=(float scalar) { + x *= scalar; + y *= scalar; + return *this; + } + + Vec2& operator/=(float scalar) { + x /= scalar; + y /= scalar; + return *this; + } + + float dot(const Vec2& other) const { + return x * other.x + y * other.y; + } + + float length() const { + return std::sqrt(x * x + y * y); + } + + float lengthSquared() const { + return x * x + y * y; + } + + float distance(const Vec2& other) const { + return (*this - other).length(); + } + + float distanceSquared(const Vec2& other) const { + Vec2 diff = *this - other; + return diff.x * diff.x + diff.y * diff.y; + } + + Vec2 normalized() const { + float len = length(); + if (len > 0) { + return *this / len; + } + return *this; + } + + bool operator==(const Vec2& other) const { + return x == other.x && y == other.y; + } + + bool operator!=(const Vec2& other) const { + return x != other.x || y != other.y; + } + + bool operator<(const Vec2& other) const { + return (x < other.x) || (x == other.x && y < other.y); + } + + bool operator<=(const Vec2& other) const { + return (x < other.x) || (x == other.x && y <= other.y); + } + + bool operator>(const Vec2& other) const { + return (x > other.x) || (x == other.x && y > other.y); + } + + bool operator>=(const Vec2& other) const { + return (x > other.x) || (x == other.x && y >= other.y); + } + + Vec2 abs() const { + return Vec2(std::abs(x), std::abs(y)); + } + + Vec2 floor() const { + return Vec2(std::floor(x), std::floor(y)); + } + + Vec2 ceil() const { + return Vec2(std::ceil(x), std::ceil(y)); + } + + Vec2 round() const { + return Vec2(std::round(x), std::round(y)); + } + + Vec2 min(const Vec2& other) const { + return Vec2(std::min(x, other.x), std::min(y, other.y)); + } + + Vec2 max(const Vec2& other) const { + return Vec2(std::max(x, other.x), std::max(y, other.y)); + } + + Vec2 clamp(const Vec2& minVal, const Vec2& maxVal) const { + return Vec2( + std::clamp(x, minVal.x, maxVal.x), + std::clamp(y, minVal.y, maxVal.y) + ); + } + + Vec2 clamp(float minVal, float maxVal) const { + return Vec2( + std::clamp(x, minVal, maxVal), + std::clamp(y, minVal, maxVal) + ); + } + + bool isZero(float epsilon = 1e-10f) const { + return std::abs(x) < epsilon && std::abs(y) < epsilon; + } + + bool equals(const Vec2& other, float epsilon = 1e-10f) const { + return std::abs(x - other.x) < epsilon && + std::abs(y - other.y) < epsilon; + } + + friend Vec2 operator+(float scalar, const Vec2& vec) { + return Vec2(scalar + vec.x, scalar + vec.y); + } + + friend Vec2 operator-(float scalar, const Vec2& vec) { + return Vec2(scalar - vec.x, scalar - vec.y); + } + + friend Vec2 operator*(float scalar, const Vec2& vec) { + return Vec2(scalar * vec.x, scalar * vec.y); + } + + friend Vec2 operator/(float scalar, const Vec2& vec) { + return Vec2(scalar / vec.x, scalar / vec.y); + } + + Vec2 perpendicular() const { + return Vec2(-y, x); + } + + Vec2 reflect(const Vec2& normal) const { + return *this - 2.0f * this->dot(normal) * normal; + } + + Vec2 lerp(const Vec2& other, float t) const { + t = std::clamp(t, 0.0f, 1.0f); + return *this + (other - *this) * t; + } + + Vec2 slerp(const Vec2& other, float t) const { + t = std::clamp(t, 0.0f, 1.0f); + float dot = this->dot(other); + dot = std::clamp(dot, -1.0f, 1.0f); + + float theta = std::acos(dot) * t; + Vec2 relative = other - *this * dot; + relative = relative.normalized(); + + return (*this * std::cos(theta)) + (relative * std::sin(theta)); + } + + Vec2 rotate(float angle) const { + float cosA = std::cos(angle); + float sinA = std::sin(angle); + return Vec2(x * cosA - y * sinA, x * sinA + y * cosA); + } + + float angle() const { + return std::atan2(y, x); + } + + float angleTo(const Vec2& other) const { + return std::acos(this->dot(other) / (this->length() * other.length())); + } + + float& operator[](int index) { + return (&x)[index]; + } + + const float& operator[](int index) const { + return (&x)[index]; + } + + std::string toString() const { + return "(" + std::to_string(x) + ", " + std::to_string(y) + ")"; + } + +}; + +inline std::ostream& operator<<(std::ostream& os, const Vec2& vec) { + os << vec.toString(); + return os; +} + +namespace std { + template<> + struct hash { + size_t operator()(const Vec2& v) const { + return hash()(v.x) ^ (hash()(v.y) << 1); + } + }; +} + +#endif \ No newline at end of file diff --git a/util/bmpwriter.hpp b/util/bmpwriter.hpp new file mode 100644 index 0000000..95a1830 --- /dev/null +++ b/util/bmpwriter.hpp @@ -0,0 +1,167 @@ +#ifndef BMP_WRITER_HPP +#define BMP_WRITER_HPP + +#include +#include +#include +#include +#include +#include "vec3.hpp" + +class BMPWriter { +private: + #pragma pack(push, 1) + struct BMPHeader { + uint16_t signature = 0x4D42; // "BM" + uint32_t fileSize; + uint16_t reserved1 = 0; + uint16_t reserved2 = 0; + uint32_t dataOffset = 54; + }; + + struct BMPInfoHeader { + uint32_t headerSize = 40; + int32_t width; + int32_t height; + uint16_t planes = 1; + uint16_t bitsPerPixel = 24; + uint32_t compression = 0; + uint32_t imageSize; + int32_t xPixelsPerMeter = 0; + int32_t yPixelsPerMeter = 0; + uint32_t colorsUsed = 0; + uint32_t importantColors = 0; + }; + #pragma pack(pop) + +public: + // Save a 2D vector of Vec3 (RGB) colors as BMP + // Vec3 components: x = red, y = green, z = blue (values in range [0,1]) + static bool saveBMP(const std::string& filename, const std::vector>& pixels) { + if (pixels.empty() || pixels[0].empty()) { + return false; + } + + int height = static_cast(pixels.size()); + int width = static_cast(pixels[0].size()); + + // Validate that all rows have the same width + for (const auto& row : pixels) { + if (row.size() != width) { + return false; + } + } + + return saveBMP(filename, pixels, width, height); + } + + // Alternative interface with width/height and flat vector (row-major order) + static bool saveBMP(const std::string& filename, const std::vector& pixels, int width, int height) { + if (pixels.size() != width * height) { + return false; + } + + // Convert to 2D vector format + std::vector> pixels2D(height, std::vector(width)); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + pixels2D[y][x] = pixels[y * width + x]; + } + } + + return saveBMP(filename, pixels2D, width, height); + } + + // Save from 1D vector of uint8_t pixels (BGR order: pixels[i]=b, pixels[i+1]=g, pixels[i+2]=r) + static bool saveBMP(const std::string& filename, const std::vector& pixels, int width, int height) { + if (pixels.size() != width * height * 3) { + return false; + } + + BMPHeader header; + BMPInfoHeader infoHeader; + + int rowSize = (width * 3 + 3) & ~3; // 24-bit, padded to 4 bytes + int imageSize = rowSize * height; + + header.fileSize = sizeof(BMPHeader) + sizeof(BMPInfoHeader) + imageSize; + infoHeader.width = width; + infoHeader.height = height; + infoHeader.imageSize = imageSize; + + std::ofstream file(filename, std::ios::binary); + if (!file) { + return false; + } + + file.write(reinterpret_cast(&header), sizeof(header)); + file.write(reinterpret_cast(&infoHeader), sizeof(infoHeader)); + + // Write pixel data (BMP stores pixels bottom-to-top) + std::vector row(rowSize, 0); + for (int y = height - 1; y >= 0; --y) { + const uint8_t* srcRow = pixels.data() + (y * width * 3); + + // Copy and rearrange if necessary (input is already in BGR order) + for (int x = 0; x < width; ++x) { + int srcOffset = x * 3; + int dstOffset = x * 3; + + // Input is already BGR: pixels[i]=b, pixels[i+1]=g, pixels[i+2]=r + // So we can copy directly + row[dstOffset] = srcRow[srcOffset]; // B + row[dstOffset + 1] = srcRow[srcOffset + 1]; // G + row[dstOffset + 2] = srcRow[srcOffset + 2]; // R + } + file.write(reinterpret_cast(row.data()), rowSize); + } + + return true; + } + +private: + static bool saveBMP(const std::string& filename, const std::vector>& pixels, int width, int height) { + BMPHeader header; + BMPInfoHeader infoHeader; + + int rowSize = (width * 3 + 3) & ~3; // 24-bit, padded to 4 bytes + int imageSize = rowSize * height; + + header.fileSize = sizeof(BMPHeader) + sizeof(BMPInfoHeader) + imageSize; + infoHeader.width = width; + infoHeader.height = height; + infoHeader.imageSize = imageSize; + + std::ofstream file(filename, std::ios::binary); + if (!file) { + return false; + } + + file.write(reinterpret_cast(&header), sizeof(header)); + file.write(reinterpret_cast(&infoHeader), sizeof(infoHeader)); + + // Write pixel data (BMP stores pixels bottom-to-top) + std::vector row(rowSize, 0); + for (int y = height - 1; y >= 0; --y) { + for (int x = 0; x < width; ++x) { + const Vec3& color = pixels[y][x]; + + // Convert from [0,1] float to [0,255] uint8_t + uint8_t r = static_cast(std::clamp(color.x * 255.0f, 0.0f, 255.0f)); + uint8_t g = static_cast(std::clamp(color.y * 255.0f, 0.0f, 255.0f)); + uint8_t b = static_cast(std::clamp(color.z * 255.0f, 0.0f, 255.0f)); + + // BMP is BGR order + int pixelOffset = x * 3; + row[pixelOffset] = b; + row[pixelOffset + 1] = g; + row[pixelOffset + 2] = r; + } + file.write(reinterpret_cast(row.data()), rowSize); + } + + return true; + } +}; + +#endif \ No newline at end of file diff --git a/util/grid2.hpp b/util/grid2.hpp new file mode 100644 index 0000000..70684b4 --- /dev/null +++ b/util/grid2.hpp @@ -0,0 +1,231 @@ +#ifndef GRID2_HPP +#define GRID2_HPP + +#include "Vec2.hpp" +#include "Vec4.hpp" +#include +#include +#include +#include + +class Grid2 { +public: + std::vector positions; + std::vector colors; + + Grid2() = default; + + // Constructor with initial size + Grid2(size_t size) { + positions.resize(size); + colors.resize(size); + } + + // Add a point with position and color + void addPoint(const Vec2& position, const Vec4& color) { + positions.push_back(position); + colors.push_back(color); + } + + // Clear all points + void clear() { + positions.clear(); + colors.clear(); + } + + // Get number of points + size_t size() const { + return positions.size(); + } + + // Check if grid is empty + bool empty() const { + return positions.empty(); + } + + // Resize the grid + void resize(size_t newSize) { + positions.resize(newSize); + colors.resize(newSize); + } + + // Render to RGB image data + std::vector renderToRGB(int width, int height, const Vec4& backgroundColor = Vec4(0, 0, 0, 1)) const { + if (width <= 0 || height <= 0) { + throw std::invalid_argument("Width and height must be positive"); + } + + std::vector imageData(width * height * 3); + + // Initialize with background color + uint8_t bgR, bgG, bgB; + backgroundColor.toUint8(bgR, bgG, bgB); + + for (int i = 0; i < width * height * 3; i += 3) { + imageData[i] = bgR; + imageData[i + 1] = bgG; + imageData[i + 2] = bgB; + } + + // Find the bounding box of all points to map to pixel coordinates + if (positions.empty()) { + return imageData; + } + + Vec2 minPos = positions[0]; + Vec2 maxPos = positions[0]; + + for (const auto& pos : positions) { + minPos = minPos.min(pos); + maxPos = maxPos.max(pos); + } + + // Add a small margin to avoid division by zero and edge issues + Vec2 size = maxPos - minPos; + if (size.x < 1e-10f) size.x = 1.0f; + if (size.y < 1e-10f) size.y = 1.0f; + + float margin = 0.05f; // 5% margin + minPos -= size * margin; + maxPos += size * margin; + size = maxPos - minPos; + + // Render each point + for (size_t i = 0; i < positions.size(); i++) { + const Vec2& pos = positions[i]; + const Vec4& color = colors[i]; + + // Convert world coordinates to pixel coordinates + float normalizedX = (pos.x - minPos.x) / size.x; + float normalizedY = 1.0f - (pos.y - minPos.y) / size.y; // Flip Y for image coordinates + + int pixelX = static_cast(normalizedX * width); + int pixelY = static_cast(normalizedY * height); + + // Clamp to image bounds + pixelX = std::clamp(pixelX, 0, width - 1); + pixelY = std::clamp(pixelY, 0, height - 1); + + // Convert color to RGB + uint8_t r, g, b; + color.toUint8(r, g, b); + + // Set pixel color + int index = (pixelY * width + pixelX) * 3; + imageData[index] = r; + imageData[index + 1] = g; + imageData[index + 2] = b; + } + + return imageData; + } + + // Render to RGBA image data (with alpha channel) + std::vector renderToRGBA(int width, int height, const Vec4& backgroundColor = Vec4(0, 0, 0, 1)) const { + if (width <= 0 || height <= 0) { + throw std::invalid_argument("Width and height must be positive"); + } + + std::vector imageData(width * height * 4); + + // Initialize with background color + uint8_t bgR, bgG, bgB, bgA; + backgroundColor.toUint8(bgR, bgG, bgB, bgA); + + for (int i = 0; i < width * height * 4; i += 4) { + imageData[i] = bgR; + imageData[i + 1] = bgG; + imageData[i + 2] = bgB; + imageData[i + 3] = bgA; + } + + if (positions.empty()) { + return imageData; + } + + // Find the bounding box (same as RGB version) + Vec2 minPos = positions[0]; + Vec2 maxPos = positions[0]; + + for (const auto& pos : positions) { + minPos = minPos.min(pos); + maxPos = maxPos.max(pos); + } + + Vec2 size = maxPos - minPos; + if (size.x < 1e-10f) size.x = 1.0f; + if (size.y < 1e-10f) size.y = 1.0f; + + float margin = 0.05f; + minPos -= size * margin; + maxPos += size * margin; + size = maxPos - minPos; + + // Render each point + for (size_t i = 0; i < positions.size(); i++) { + const Vec2& pos = positions[i]; + const Vec4& color = colors[i]; + + float normalizedX = (pos.x - minPos.x) / size.x; + float normalizedY = 1.0f - (pos.y - minPos.y) / size.y; + + int pixelX = static_cast(normalizedX * width); + int pixelY = static_cast(normalizedY * height); + + pixelX = std::clamp(pixelX, 0, width - 1); + pixelY = std::clamp(pixelY, 0, height - 1); + + uint8_t r, g, b, a; + color.toUint8(r, g, b, a); + + int index = (pixelY * width + pixelX) * 4; + imageData[index] = r; + imageData[index + 1] = g; + imageData[index + 2] = b; + imageData[index + 3] = a; + } + + return imageData; + } + + // Get the bounding box of all positions + void getBoundingBox(Vec2& minPos, Vec2& maxPos) const { + if (positions.empty()) { + minPos = Vec2(0, 0); + maxPos = Vec2(0, 0); + return; + } + + minPos = positions[0]; + maxPos = positions[0]; + + for (const auto& pos : positions) { + minPos = minPos.min(pos); + maxPos = maxPos.max(pos); + } + } + + // Scale all positions to fit within a specified range + void normalizePositions(const Vec2& targetMin = Vec2(-1, -1), const Vec2& targetMax = Vec2(1, 1)) { + if (positions.empty()) return; + + Vec2 currentMin, currentMax; + getBoundingBox(currentMin, currentMax); + + Vec2 currentSize = currentMax - currentMin; + Vec2 targetSize = targetMax - targetMin; + + if (currentSize.x < 1e-10f) currentSize.x = 1.0f; + if (currentSize.y < 1e-10f) currentSize.y = 1.0f; + + for (auto& pos : positions) { + float normalizedX = (pos.x - currentMin.x) / currentSize.x; + float normalizedY = (pos.y - currentMin.y) / currentSize.y; + + pos.x = targetMin.x + normalizedX * targetSize.x; + pos.y = targetMin.y + normalizedY * targetSize.y; + } + } +}; + +#endif \ No newline at end of file diff --git a/util/ray.cpp b/util/ray.cpp new file mode 100644 index 0000000..f50f0f8 --- /dev/null +++ b/util/ray.cpp @@ -0,0 +1,27 @@ +#ifndef RAY_HPP +#define RAY_HPP + +#include "Vec2.hpp" +#include "Vec3.hpp" +#include "Vec4.hpp" +#include "ray2.hpp" +#include "ray3.hpp" +#include "ray4.hpp" + +// Stream operators for rays +inline std::ostream& operator<<(std::ostream& os, const Ray2& ray) { + os << ray.toString(); + return os; +} + +inline std::ostream& operator<<(std::ostream& os, const Ray3& ray) { + os << ray.toString(); + return os; +} + +inline std::ostream& operator<<(std::ostream& os, const Ray4& ray) { + os << ray.toString(); + return os; +} + +#endif \ No newline at end of file diff --git a/util/ray2.hpp b/util/ray2.hpp new file mode 100644 index 0000000..645662d --- /dev/null +++ b/util/ray2.hpp @@ -0,0 +1,59 @@ +#ifndef RAY2_HPP +#define RAY2_HPP + +#include "Vec2.hpp" + +class Ray2 { +public: + Vec2 origin; + Vec2 direction; + + Ray2() : origin(Vec2()), direction(Vec2(1, 0)) {} + Ray2(const Vec2& origin, const Vec2& direction) + : origin(origin), direction(direction.normalized()) {} + + // Get point at parameter t along the ray + Vec2 at(float t) const { + return origin + direction * t; + } + + // Reflect ray off a surface with given normal + Ray2 reflect(const Vec2& point, const Vec2& normal) const { + Vec2 reflectedDir = direction.reflect(normal); + return Ray2(point, reflectedDir); + } + + // Check if ray intersects with a circle + bool intersectsCircle(const Vec2& center, float radius, float& t1, float& t2) const { + Vec2 oc = origin - center; + float a = direction.dot(direction); + float b = 2.0f * oc.dot(direction); + float c = oc.dot(oc) - radius * radius; + + float discriminant = b * b - 4 * a * c; + + if (discriminant < 0) { + return false; + } + + discriminant = std::sqrt(discriminant); + t1 = (-b - discriminant) / (2.0f * a); + t2 = (-b + discriminant) / (2.0f * a); + + return true; + } + + // Get the distance from a point to this ray + float distanceToPoint(const Vec2& point) const { + Vec2 pointToOrigin = point - origin; + float projection = pointToOrigin.dot(direction); + Vec2 closestPoint = origin + direction * projection; + return point.distance(closestPoint); + } + + std::string toString() const { + return "Ray2(origin: " + origin.toString() + ", direction: " + direction.toString() + ")"; + } +}; + +#endif \ No newline at end of file diff --git a/util/ray3.hpp b/util/ray3.hpp new file mode 100644 index 0000000..8b8cd67 --- /dev/null +++ b/util/ray3.hpp @@ -0,0 +1,73 @@ +#ifndef RAY3_HPP +#define RAY3_HPP + +#include "Vec3.hpp" + +class Ray3 { +public: + Vec3 origin; + Vec3 direction; + + Ray3() : origin(Vec3()), direction(Vec3(1, 0, 0)) {} + Ray3(const Vec3& origin, const Vec3& direction) + : origin(origin), direction(direction.normalized()) {} + + // Get point at parameter t along the ray + Vec3 at(float t) const { + return origin + direction * t; + } + + // Reflect ray off a surface with given normal + Ray3 reflect(const Vec3& point, const Vec3& normal) const { + Vec3 reflectedDir = direction.reflect(normal); + return Ray3(point, reflectedDir); + } + + // Check if ray intersects with a sphere + bool intersectsSphere(const Vec3& center, float radius, float& t1, float& t2) const { + Vec3 oc = origin - center; + float a = direction.dot(direction); + float b = 2.0f * oc.dot(direction); + float c = oc.dot(oc) - radius * radius; + + float discriminant = b * b - 4 * a * c; + + if (discriminant < 0) { + return false; + } + + discriminant = std::sqrt(discriminant); + t1 = (-b - discriminant) / (2.0f * a); + t2 = (-b + discriminant) / (2.0f * a); + + return true; + } + + // Check if ray intersects with a plane (defined by point and normal) + bool intersectsPlane(const Vec3& planePoint, const Vec3& planeNormal, float& t) const { + float denom = planeNormal.dot(direction); + + if (std::abs(denom) < 1e-10f) { + return false; // Ray is parallel to plane + } + + t = planeNormal.dot(planePoint - origin) / denom; + return t >= 0; + } + + // Get the distance from a point to this ray + float distanceToPoint(const Vec3& point) const { + Vec3 pointToOrigin = point - origin; + Vec3 crossProduct = direction.cross(pointToOrigin); + return crossProduct.length() / direction.length(); + } + + // Transform ray by a 4x4 matrix (for perspective/affine transformations) + Ray3 transform(const class Mat4& matrix) const; + + std::string toString() const { + return "Ray3(origin: " + origin.toString() + ", direction: " + direction.toString() + ")"; + } +}; + +#endif \ No newline at end of file diff --git a/util/ray4.hpp b/util/ray4.hpp new file mode 100644 index 0000000..646b4dc --- /dev/null +++ b/util/ray4.hpp @@ -0,0 +1,52 @@ +#ifndef RAY4_HPP +#define RAY4_HPP + +#include "vec4.hpp" + +class Ray4 { +public: + Vec4 origin; + Vec4 direction; + + Ray4() : origin(Vec4()), direction(Vec4(1, 0, 0, 0)) {} + Ray4(const Vec4& origin, const Vec4& direction) + : origin(origin), direction(direction.normalized()) {} + + // Get point at parameter t along the ray (in 4D space) + Vec4 at(float t) const { + return origin + direction * t; + } + + // Get 3D projection of the ray (homogeneous coordinates) + // Ray3 projectTo3D() const { + // Vec3 projOrigin = origin.homogenized().xyz(); + // Vec3 projDirection = direction.homogenized().xyz().normalized(); + // return Ray3(projOrigin, projDirection); + // } + + // Get the distance from a point to this ray in 4D space + float distanceToPoint(const Vec4& point) const { + Vec4 pointToOrigin = point - origin; + float projection = pointToOrigin.dot(direction); + Vec4 closestPoint = origin + direction * projection; + return point.distance(closestPoint); + } + + // Check if this 4D ray intersects with a 3D hyperplane + bool intersectsHyperplane(const Vec4& planePoint, const Vec4& planeNormal, float& t) const { + float denom = planeNormal.dot(direction); + + if (std::abs(denom) < 1e-10f) { + return false; // Ray is parallel to hyperplane + } + + t = planeNormal.dot(planePoint - origin) / denom; + return true; + } + + std::string toString() const { + return "Ray4(origin: " + origin.toString() + ", direction: " + direction.toString() + ")"; + } +}; + +#endif \ No newline at end of file diff --git a/util/timing_decorator.cpp b/util/timing_decorator.cpp new file mode 100644 index 0000000..b8ef2af --- /dev/null +++ b/util/timing_decorator.cpp @@ -0,0 +1,115 @@ +#include "timing_decorator.hpp" +#include + +std::unordered_map FunctionTimer::stats_; + +void FunctionTimer::recordTiming(const std::string& func_name, double elapsed_seconds) { + auto& stat = stats_[func_name]; + stat.call_count++; + stat.total_time += elapsed_seconds; + stat.timings.push_back(elapsed_seconds); +} + +FunctionTimer::PercentileStats FunctionTimer::calculatePercentiles(const std::vector& timings) { + PercentileStats result; + if (timings.empty()) return result; + + std::vector sorted_timings = timings; + std::sort(sorted_timings.begin(), sorted_timings.end()); + + auto percentile_index = [&](double p) -> size_t { + return std::min(sorted_timings.size() - 1, + static_cast(p * sorted_timings.size() / 100.0)); + }; + + result.min = sorted_timings.front(); + result.max = sorted_timings.back(); + result.median = sorted_timings[percentile_index(50.0)]; + result.p90 = sorted_timings[percentile_index(90.0)]; + result.p95 = sorted_timings[percentile_index(95.0)]; + result.p99 = sorted_timings[percentile_index(99.0)]; + result.p99_9 = sorted_timings[percentile_index(99.9)]; + + return result; +} + +std::unordered_map FunctionTimer::getStats(Mode mode) { + return stats_; // In C++ we return all data, filtering happens in print +} + +void FunctionTimer::printStats(Mode mode) { + if (stats_.empty()) { + std::cout << "No timing statistics available." << std::endl; + return; + } + + // Determine column widths + size_t func_col_width = 0; + for (const auto& [name, _] : stats_) { + func_col_width = std::max(func_col_width, name.length()); + } + func_col_width = std::max(func_col_width, size_t(8)); // "Function" + + const int num_width = 12; + + if (mode == Mode::BASIC) { + std::cout << "\nBasic Function Timing Statistics:" << std::endl; + std::cout << std::string(func_col_width + 3 * num_width + 8, '-') << std::endl; + + std::cout << std::left << std::setw(func_col_width) << "Function" + << std::setw(num_width) << "Calls" + << std::setw(num_width) << "Total (s)" + << std::setw(num_width) << "Avg (s)" << std::endl; + + std::cout << std::string(func_col_width + 3 * num_width + 8, '-') << std::endl; + + for (const auto& [func_name, data] : stats_) { + if (data.call_count > 0) { + std::cout << std::left << std::setw(func_col_width) << func_name + << std::setw(num_width) << data.call_count + << std::setw(num_width) << std::fixed << std::setprecision(6) << data.total_time + << std::setw(num_width) << std::fixed << std::setprecision(6) << data.avg_time() << std::endl; + } + } + + std::cout << std::string(func_col_width + 3 * num_width + 8, '-') << std::endl; + } else { // ENHANCED mode + std::cout << "\nEnhanced Function Timing Statistics:" << std::endl; + size_t total_width = func_col_width + 8 * num_width + 8; + std::cout << std::string(total_width, '-') << std::endl; + + std::cout << std::left << std::setw(func_col_width) << "Function" + << std::setw(num_width) << "Calls" + << std::setw(num_width) << "Total (s)" + << std::setw(num_width) << "Avg (s)" + << std::setw(num_width) << "Min (s)" + << std::setw(num_width) << "Median (s)" + << std::setw(num_width) << "P99 (s)" + << std::setw(num_width) << "P99.9 (s)" + << std::setw(num_width) << "Max (s)" << std::endl; + + std::cout << std::string(total_width, '-') << std::endl; + + for (const auto& [func_name, data] : stats_) { + if (data.call_count > 0) { + auto percentiles = calculatePercentiles(data.timings); + + std::cout << std::left << std::setw(func_col_width) << func_name + << std::setw(num_width) << data.call_count + << std::setw(num_width) << std::fixed << std::setprecision(6) << data.total_time + << std::setw(num_width) << std::fixed << std::setprecision(6) << data.avg_time() + << std::setw(num_width) << std::fixed << std::setprecision(6) << percentiles.min + << std::setw(num_width) << std::fixed << std::setprecision(6) << percentiles.median + << std::setw(num_width) << std::fixed << std::setprecision(6) << percentiles.p99 + << std::setw(num_width) << std::fixed << std::setprecision(6) << percentiles.p99_9 + << std::setw(num_width) << std::fixed << std::setprecision(6) << percentiles.max << std::endl; + } + } + + std::cout << std::string(total_width, '-') << std::endl; + } +} + +void FunctionTimer::clearStats() { + stats_.clear(); +} \ No newline at end of file diff --git a/util/timing_decorator.hpp b/util/timing_decorator.hpp new file mode 100644 index 0000000..218ae86 --- /dev/null +++ b/util/timing_decorator.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class FunctionTimer { +public: + enum class Mode { BASIC, ENHANCED }; + + struct TimingStats { + size_t call_count = 0; + double total_time = 0.0; + std::vector timings; + + double avg_time() const { + return call_count > 0 ? total_time / call_count : 0.0; + } + }; + + struct PercentileStats { + double p99_9 = 0.0; + double p99 = 0.0; + double p95 = 0.0; + double p90 = 0.0; + double max = 0.0; + double min = 0.0; + double median = 0.0; + }; + + // Record timing for a function + static void recordTiming(const std::string& func_name, double elapsed_seconds); + + // Get statistics + static std::unordered_map getStats(Mode mode = Mode::BASIC); + + // Print statistics + static void printStats(Mode mode = Mode::ENHANCED); + + // Clear all statistics + static void clearStats(); + +private: + static std::unordered_map stats_; + + static PercentileStats calculatePercentiles(const std::vector& timings); +}; + +// Macro to easily time functions - similar to Python decorator +#define TIME_FUNCTION auto function_timer_scoped_ = ScopedFunctionTimer(__func__) + +// Scoped timer for RAII-style timing +class ScopedFunctionTimer { +public: + ScopedFunctionTimer(const std::string& func_name) + : func_name_(func_name), start_(std::chrono::steady_clock::now()) {} + + ~ScopedFunctionTimer() { + auto end = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start_); + FunctionTimer::recordTiming(func_name_, elapsed.count() / 1000000.0); + } + +private: + std::string func_name_; + std::chrono::steady_clock::time_point start_; +}; + +// Template decorator for functions (similar to Python) +template +auto time_function_decorator(const std::string& func_name, Func&& func, Args&&... args) { + auto start = std::chrono::steady_clock::now(); + + if constexpr (std::is_void_v>) { + // Void return type + std::invoke(std::forward(func), std::forward(args)...); + auto end = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start); + FunctionTimer::recordTiming(func_name, elapsed.count() / 1000000.0); + } else { + // Non-void return type + auto result = std::invoke(std::forward(func), std::forward(args)...); + auto end = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start); + FunctionTimer::recordTiming(func_name, elapsed.count() / 1000000.0); + return result; + } +} + +// Macro to create decorated functions +#define DECORATE_FUNCTION(func) [&](auto&&... args) { \ + return time_function_decorator(#func, func, std::forward(args)...); \ +} diff --git a/util/vec.cpp b/util/vec.cpp new file mode 100644 index 0000000..4df7aaf --- /dev/null +++ b/util/vec.cpp @@ -0,0 +1,19 @@ +#ifndef vec_hpp +#define vec_hpp + +#include "Vec4.hpp" +#include "Vec3.hpp" +#include "Vec2.hpp" + +Vec4::Vec4(const Vec3& vec3, float w) : x(vec3.x), y(vec3.y), z(vec3.z), w(w) {} +Vec3::Vec3(const Vec2& vec2, float z) : x(vec2.x), y(vec2.y), z(z) {} + +Vec3 Vec4::xyz() const { + return Vec3(x, y, z); +} + +Vec3 Vec4::rgb() const { + return Vec3(r, g, b); +} + +#endif \ No newline at end of file diff --git a/util/vec3.hpp b/util/vec3.hpp new file mode 100644 index 0000000..d0d6020 --- /dev/null +++ b/util/vec3.hpp @@ -0,0 +1,322 @@ +#ifndef VEC3_HPP +#define VEC3_HPP + +#include +#include +#include +#include + +class Vec3 { +public: + float x, y, z; + + Vec3() : x(0), y(0), z(0) {} + Vec3(float x, float y, float z) : x(x), y(y), z(z) {} + Vec3(float scalar) : x(scalar), y(scalar), z(scalar) {} + + Vec3(const class Vec2& vec2, float z = 0.0f); + + // Arithmetic operations + Vec3 operator+(const Vec3& other) const { + return Vec3(x + other.x, y + other.y, z + other.z); + } + + Vec3 operator-(const Vec3& other) const { + return Vec3(x - other.x, y - other.y, z - other.z); + } + + Vec3 operator*(const Vec3& other) const { + return Vec3(x * other.x, y * other.y, z * other.z); + } + + Vec3 operator/(const Vec3& other) const { + return Vec3(x / other.x, y / other.y, z / other.z); + } + + Vec3 operator+(float scalar) const { + return Vec3(x + scalar, y + scalar, z + scalar); + } + + Vec3 operator-(float scalar) const { + return Vec3(x - scalar, y - scalar, z - scalar); + } + + Vec3 operator-() const { + return Vec3(-x, -y, -z); + } + + Vec3 operator*(float scalar) const { + return Vec3(x * scalar, y * scalar, z * scalar); + } + + Vec3 operator/(float scalar) const { + return Vec3(x / scalar, y / scalar, z / scalar); + } + + Vec3& operator=(float scalar) { + x = y = z = scalar; + return *this; + } + + Vec3& operator+=(const Vec3& other) { + x += other.x; + y += other.y; + z += other.z; + return *this; + } + + Vec3& operator-=(const Vec3& other) { + x -= other.x; + y -= other.y; + z -= other.z; + return *this; + } + + Vec3& operator*=(const Vec3& other) { + x *= other.x; + y *= other.y; + z *= other.z; + return *this; + } + + Vec3& operator/=(const Vec3& other) { + x /= other.x; + y /= other.y; + z /= other.z; + return *this; + } + + Vec3& operator+=(float scalar) { + x += scalar; + y += scalar; + z += scalar; + return *this; + } + + Vec3& operator-=(float scalar) { + x -= scalar; + y -= scalar; + z -= scalar; + return *this; + } + + Vec3& operator*=(float scalar) { + x *= scalar; + y *= scalar; + z *= scalar; + return *this; + } + + Vec3& operator/=(float scalar) { + x /= scalar; + y /= scalar; + z /= scalar; + return *this; + } + + float dot(const Vec3& other) const { + return x * other.x + y * other.y + z * other.z; + } + + Vec3 cross(const Vec3& other) const { + return Vec3( + y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x + ); + } + + float length() const { + return std::sqrt(x * x + y * y + z * z); + } + + float lengthSquared() const { + return x * x + y * y + z * z; + } + + float distance(const Vec3& other) const { + return (*this - other).length(); + } + + float distanceSquared(const Vec3& other) const { + Vec3 diff = *this - other; + return diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; + } + + Vec3 normalized() const { + float len = length(); + if (len > 0) { + return *this / len; + } + return *this; + } + + bool operator==(const Vec3& other) const { + return x == other.x && y == other.y && z == other.z; + } + + bool operator!=(const Vec3& other) const { + return x != other.x || y != other.y || z != other.z; + } + + bool operator<(const Vec3& other) const { + return (x < other.x) || + (x == other.x && y < other.y) || + (x == other.x && y == other.y && z < other.z); + } + + bool operator<=(const Vec3& other) const { + return (x < other.x) || + (x == other.x && y < other.y) || + (x == other.x && y == other.y && z <= other.z); + } + + bool operator>(const Vec3& other) const { + return (x > other.x) || + (x == other.x && y > other.y) || + (x == other.x && y == other.y && z > other.z); + } + + bool operator>=(const Vec3& other) const { + return (x > other.x) || + (x == other.x && y > other.y) || + (x == other.x && y == other.y && z >= other.z); + } + + Vec3 abs() const { + return Vec3(std::abs(x), std::abs(y), std::abs(z)); + } + + Vec3 floor() const { + return Vec3(std::floor(x), std::floor(y), std::floor(z)); + } + + Vec3 ceil() const { + return Vec3(std::ceil(x), std::ceil(y), std::ceil(z)); + } + + Vec3 round() const { + return Vec3(std::round(x), std::round(y), std::round(z)); + } + + Vec3 min(const Vec3& other) const { + return Vec3(std::min(x, other.x), std::min(y, other.y), std::min(z, other.z)); + } + + Vec3 max(const Vec3& other) const { + return Vec3(std::max(x, other.x), std::max(y, other.y), std::max(z, other.z)); + } + + Vec3 clamp(const Vec3& minVal, const Vec3& maxVal) const { + return Vec3( + std::clamp(x, minVal.x, maxVal.x), + std::clamp(y, minVal.y, maxVal.y), + std::clamp(z, minVal.z, maxVal.z) + ); + } + + Vec3 clamp(float minVal, float maxVal) const { + return Vec3( + std::clamp(x, minVal, maxVal), + std::clamp(y, minVal, maxVal), + std::clamp(z, minVal, maxVal) + ); + } + + bool isZero(float epsilon = 1e-10f) const { + return std::abs(x) < epsilon && std::abs(y) < epsilon && std::abs(z) < epsilon; + } + + bool equals(const Vec3& other, float epsilon = 1e-10f) const { + return std::abs(x - other.x) < epsilon && + std::abs(y - other.y) < epsilon && + std::abs(z - other.z) < epsilon; + } + + friend Vec3 operator+(float scalar, const Vec3& vec) { + return Vec3(scalar + vec.x, scalar + vec.y, scalar + vec.z); + } + + friend Vec3 operator-(float scalar, const Vec3& vec) { + return Vec3(scalar - vec.x, scalar - vec.y, scalar - vec.z); + } + + friend Vec3 operator*(float scalar, const Vec3& vec) { + return Vec3(scalar * vec.x, scalar * vec.y, scalar * vec.z); + } + + friend Vec3 operator/(float scalar, const Vec3& vec) { + return Vec3(scalar / vec.x, scalar / vec.y, scalar / vec.z); + } + + Vec3 reflect(const Vec3& normal) const { + return *this - 2.0f * this->dot(normal) * normal; + } + + Vec3 lerp(const Vec3& other, float t) const { + t = std::clamp(t, 0.0f, 1.0f); + return *this + (other - *this) * t; + } + + Vec3 slerp(const Vec3& other, float t) const { + t = std::clamp(t, 0.0f, 1.0f); + float dot = this->dot(other); + dot = std::clamp(dot, -1.0f, 1.0f); + + float theta = std::acos(dot) * t; + Vec3 relative = other - *this * dot; + relative = relative.normalized(); + + return (*this * std::cos(theta)) + (relative * std::sin(theta)); + } + + Vec3 rotateX(float angle) const { + float cosA = std::cos(angle); + float sinA = std::sin(angle); + return Vec3(x, y * cosA - z * sinA, y * sinA + z * cosA); + } + + Vec3 rotateY(float angle) const { + float cosA = std::cos(angle); + float sinA = std::sin(angle); + return Vec3(x * cosA + z * sinA, y, -x * sinA + z * cosA); + } + + Vec3 rotateZ(float angle) const { + float cosA = std::cos(angle); + float sinA = std::sin(angle); + return Vec3(x * cosA - y * sinA, x * sinA + y * cosA, z); + } + + float angleTo(const Vec3& other) const { + return std::acos(this->dot(other) / (this->length() * other.length())); + } + + float& operator[](int index) { + return (&x)[index]; + } + + const float& operator[](int index) const { + return (&x)[index]; + } + + std::string toString() const { + return "(" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ")"; + } +}; + +inline std::ostream& operator<<(std::ostream& os, const Vec3& vec) { + os << vec.toString(); + return os; +} + +namespace std { + template<> + struct hash { + size_t operator()(const Vec3& v) const { + return hash()(v.x) ^ (hash()(v.y) << 1) ^ (hash()(v.z) << 2); + } + }; +} + +#endif \ No newline at end of file diff --git a/util/vec4.hpp b/util/vec4.hpp new file mode 100644 index 0000000..0fb9c1c --- /dev/null +++ b/util/vec4.hpp @@ -0,0 +1,384 @@ +#ifndef VEC4_HPP +#define VEC4_HPP + +#include "vec3.hpp" +#include +#include +#include +#include +#include + +class Vec4 { +public: + union { + struct { float x, y, z, w; }; + struct { float r, g, b, a; }; + struct { float s, t, p, q; }; // For texture coordinates + }; + + // Constructors + Vec4() : x(0), y(0), z(0), w(0) {} + Vec4(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) {} + Vec4(float scalar) : x(scalar), y(scalar), z(scalar), w(scalar) {} + + Vec4(const Vec3& rgb, float w = 1.0f) : x(rgb.x), y(rgb.y), z(rgb.z), w(w) {} + static Vec4 RGB(float r, float g, float b, float a = 1.0f) { return Vec4(r, g, b, a); } + static Vec4 RGBA(float r, float g, float b, float a) { return Vec4(r, g, b, a); } + + + Vec4 operator+(const Vec4& other) const { + return Vec4(x + other.x, y + other.y, z + other.z, w + other.w); + } + + Vec4 operator-(const Vec4& other) const { + return Vec4(x - other.x, y - other.y, z - other.z, w - other.w); + } + + Vec4 operator*(const Vec4& other) const { + return Vec4(x * other.x, y * other.y, z * other.z, w * other.w); + } + + Vec4 operator/(const Vec4& other) const { + return Vec4(x / other.x, y / other.y, z / other.z, w / other.w); + } + + Vec4 operator+(float scalar) const { + return Vec4(x + scalar, y + scalar, z + scalar, w + scalar); + } + + Vec4 operator-(float scalar) const { + return Vec4(x - scalar, y - scalar, z - scalar, w - scalar); + } + + Vec4 operator-() const { + return Vec4(-x, -y, -z, -w); + } + + Vec4 operator*(float scalar) const { + return Vec4(x * scalar, y * scalar, z * scalar, w * scalar); + } + + Vec4 operator/(float scalar) const { + return Vec4(x / scalar, y / scalar, z / scalar, w / scalar); + } + + Vec4& operator=(float scalar) { + x = y = z = w = scalar; + return *this; + } + + Vec4& operator+=(const Vec4& other) { + x += other.x; + y += other.y; + z += other.z; + w += other.w; + return *this; + } + + Vec4& operator-=(const Vec4& other) { + x -= other.x; + y -= other.y; + z -= other.z; + w -= other.w; + return *this; + } + + Vec4& operator*=(const Vec4& other) { + x *= other.x; + y *= other.y; + z *= other.z; + w *= other.w; + return *this; + } + + Vec4& operator/=(const Vec4& other) { + x /= other.x; + y /= other.y; + z /= other.z; + w /= other.w; + return *this; + } + + Vec4& operator+=(float scalar) { + x += scalar; + y += scalar; + z += scalar; + w += scalar; + return *this; + } + + Vec4& operator-=(float scalar) { + x -= scalar; + y -= scalar; + z -= scalar; + w -= scalar; + return *this; + } + + Vec4& operator*=(float scalar) { + x *= scalar; + y *= scalar; + z *= scalar; + w *= scalar; + return *this; + } + + Vec4& operator/=(float scalar) { + x /= scalar; + y /= scalar; + z /= scalar; + w /= scalar; + return *this; + } + + float dot(const Vec4& other) const { + return x * other.x + y * other.y + z * other.z + w * other.w; + } + + // 4D cross product (returns vector perpendicular to 3 given vectors in 4D space) + Vec4 cross(const Vec4& v1, const Vec4& v2, const Vec4& v3) const { + float a = v1.y * (v2.z * v3.w - v2.w * v3.z) - + v1.z * (v2.y * v3.w - v2.w * v3.y) + + v1.w * (v2.y * v3.z - v2.z * v3.y); + + float b = -v1.x * (v2.z * v3.w - v2.w * v3.z) + + v1.z * (v2.x * v3.w - v2.w * v3.x) - + v1.w * (v2.x * v3.z - v2.z * v3.x); + + float c = v1.x * (v2.y * v3.w - v2.w * v3.y) - + v1.y * (v2.x * v3.w - v2.w * v3.x) + + v1.w * (v2.x * v3.y - v2.y * v3.x); + + float d = -v1.x * (v2.y * v3.z - v2.z * v3.y) + + v1.y * (v2.x * v3.z - v2.z * v3.x) - + v1.z * (v2.x * v3.y - v2.y * v3.x); + + return Vec4(a, b, c, d); + } + + float length() const { + return std::sqrt(x * x + y * y + z * z + w * w); + } + + float lengthSquared() const { + return x * x + y * y + z * z + w * w; + } + + float distance(const Vec4& other) const { + return (*this - other).length(); + } + + float distanceSquared(const Vec4& other) const { + Vec4 diff = *this - other; + return diff.x * diff.x + diff.y * diff.y + diff.z * diff.z + diff.w * diff.w; + } + + Vec4 normalized() const { + float len = length(); + if (len > 0) { + return *this / len; + } + return *this; + } + + // Homogeneous normalization (divide by w) + Vec4 homogenized() const { + if (w != 0.0f) { + return Vec4(x / w, y / w, z / w, 1.0f); + } + return *this; + } + + // Clamp values between 0 and 1 + Vec4 clamped() const { + return Vec4( + std::clamp(r, 0.0f, 1.0f), + std::clamp(g, 0.0f, 1.0f), + std::clamp(b, 0.0f, 1.0f), + std::clamp(a, 0.0f, 1.0f) + ); + } + + // Convert to Vec3 (ignoring alpha) + Vec3 toVec3() const { + return Vec3(r, g, b); + } + + // Convert to 8-bit color values + void toUint8(uint8_t& red, uint8_t& green, uint8_t& blue, uint8_t& alpha) const { + red = static_cast(std::clamp(r, 0.0f, 1.0f) * 255); + green = static_cast(std::clamp(g, 0.0f, 1.0f) * 255); + blue = static_cast(std::clamp(b, 0.0f, 1.0f) * 255); + alpha = static_cast(std::clamp(a, 0.0f, 1.0f) * 255); + } + + void toUint8(uint8_t& red, uint8_t& green, uint8_t& blue) const { + red = static_cast(std::clamp(r, 0.0f, 1.0f) * 255); + green = static_cast(std::clamp(g, 0.0f, 1.0f) * 255); + blue = static_cast(std::clamp(b, 0.0f, 1.0f) * 255); + } + // Get XYZ components as Vec3 + class Vec3 xyz() const; + + // Get RGB components as Vec3 + class Vec3 rgb() const; + + bool operator==(const Vec4& other) const { + return x == other.x && y == other.y && z == other.z && w == other.w; + } + + bool operator!=(const Vec4& other) const { + return x != other.x || y != other.y || z != other.z || w != other.w; + } + + bool operator<(const Vec4& other) const { + return (x < other.x) || + (x == other.x && y < other.y) || + (x == other.x && y == other.y && z < other.z) || + (x == other.x && y == other.y && z == other.z && w < other.w); + } + + bool operator<=(const Vec4& other) const { + return (x < other.x) || + (x == other.x && y < other.y) || + (x == other.x && y == other.y && z < other.z) || + (x == other.x && y == other.y && z == other.z && w <= other.w); + } + + bool operator>(const Vec4& other) const { + return (x > other.x) || + (x == other.x && y > other.y) || + (x == other.x && y == other.y && z > other.z) || + (x == other.x && y == other.y && z == other.z && w > other.w); + } + + bool operator>=(const Vec4& other) const { + return (x > other.x) || + (x == other.x && y > other.y) || + (x == other.x && y == other.y && z > other.z) || + (x == other.x && y == other.y && z == other.z && w >= other.w); + } + + Vec4 abs() const { + return Vec4(std::abs(x), std::abs(y), std::abs(z), std::abs(w)); + } + + Vec4 floor() const { + return Vec4(std::floor(x), std::floor(y), std::floor(z), std::floor(w)); + } + + Vec4 ceil() const { + return Vec4(std::ceil(x), std::ceil(y), std::ceil(z), std::ceil(w)); + } + + Vec4 round() const { + return Vec4(std::round(x), std::round(y), std::round(z), std::round(w)); + } + + Vec4 min(const Vec4& other) const { + return Vec4(std::min(x, other.x), std::min(y, other.y), + std::min(z, other.z), std::min(w, other.w)); + } + + Vec4 max(const Vec4& other) const { + return Vec4(std::max(x, other.x), std::max(y, other.y), + std::max(z, other.z), std::max(w, other.w)); + } + + Vec4 clamp(float minVal, float maxVal) const { + return Vec4( + std::clamp(x, minVal, maxVal), + std::clamp(y, minVal, maxVal), + std::clamp(z, minVal, maxVal), + std::clamp(w, minVal, maxVal) + ); + } + + // Color-specific clamping (clamps RGB between 0 and 1) + Vec4 clampColor() const { + return Vec4( + std::clamp(r, 0.0f, 1.0f), + std::clamp(g, 0.0f, 1.0f), + std::clamp(b, 0.0f, 1.0f), + std::clamp(a, 0.0f, 1.0f) + ); + } + + bool isZero(float epsilon = 1e-10f) const { + return std::abs(x) < epsilon && std::abs(y) < epsilon && + std::abs(z) < epsilon && std::abs(w) < epsilon; + } + + bool equals(const Vec4& other, float epsilon = 1e-10f) const { + return std::abs(x - other.x) < epsilon && + std::abs(y - other.y) < epsilon && + std::abs(z - other.z) < epsilon && + std::abs(w - other.w) < epsilon; + } + + friend Vec4 operator+(float scalar, const Vec4& vec) { + return Vec4(scalar + vec.x, scalar + vec.y, scalar + vec.z, scalar + vec.w); + } + + friend Vec4 operator-(float scalar, const Vec4& vec) { + return Vec4(scalar - vec.x, scalar - vec.y, scalar - vec.z, scalar - vec.w); + } + + friend Vec4 operator*(float scalar, const Vec4& vec) { + return Vec4(scalar * vec.x, scalar * vec.y, scalar * vec.z, scalar * vec.w); + } + + friend Vec4 operator/(float scalar, const Vec4& vec) { + return Vec4(scalar / vec.x, scalar / vec.y, scalar / vec.z, scalar / vec.w); + } + + Vec4 lerp(const Vec4& other, float t) const { + t = std::clamp(t, 0.0f, 1.0f); + return *this + (other - *this) * t; + } + + // Convert to grayscale using standard RGB weights + float grayscale() const { + return r * 0.299f + g * 0.587f + b * 0.114f; + } + + // Color inversion (1.0 - color) + Vec4 inverted() const { + return Vec4(1.0f - r, 1.0f - g, 1.0f - b, a); + } + + float& operator[](int index) { + return (&x)[index]; + } + + const float& operator[](int index) const { + return (&x)[index]; + } + + std::string toString() const { + return "(" + std::to_string(x) + ", " + std::to_string(y) + ", " + + std::to_string(z) + ", " + std::to_string(w) + ")"; + } + + std::string toColorString() const { + return "RGBA(" + std::to_string(r) + ", " + std::to_string(g) + ", " + + std::to_string(b) + ", " + std::to_string(a) + ")"; + } +}; + +inline std::ostream& operator<<(std::ostream& os, const Vec4& vec) { + os << vec.toString(); + return os; +} + +namespace std { + template<> + struct hash { + size_t operator()(const Vec4& v) const { + return hash()(v.x) ^ (hash()(v.y) << 1) ^ + (hash()(v.z) << 2) ^ (hash()(v.w) << 3); + } + }; +} + +#endif \ No newline at end of file diff --git a/util/voxelgrid.hpp b/util/voxelgrid.hpp new file mode 100644 index 0000000..fa96836 --- /dev/null +++ b/util/voxelgrid.hpp @@ -0,0 +1,217 @@ +#ifndef VOXEL_HPP +#define VOXEL_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "timing_decorator.hpp" +#include "vec3.hpp" +#include "vec4.hpp" + +class VoxelGrid { +private: + std::unordered_map positionToIndex; + std::vector positions; + std::vector colors; + std::vector layers; + + Vec3 gridSize; + +public: + Vec3 voxelSize; + + enum LayerType { + ATMOSPHERE = 0, + CRUST = 1, + MANTLE = 2, + OUTER_CORE = 3, + INNER_CORE = 4, + EMPTY = -1 + }; + + VoxelGrid(const Vec3& size, const Vec3& voxelSize = Vec3(1, 1, 1)) : gridSize(size), voxelSize(voxelSize) {} + + void addVoxel(const Vec3& position, const Vec4& color) { + Vec3 gridPos = worldToGrid(position); + + auto it = positionToIndex.find(gridPos); + if (it == positionToIndex.end()) { + size_t index = positions.size(); + positions.push_back(gridPos); + colors.push_back(color); + layers.push_back(EMPTY); + positionToIndex[gridPos] = index; + } else { + colors[it->second] = color; + } + } + + void addVoxelWithLayer(const Vec3& position, const Vec4& color, int layer) { + Vec3 gridPos = worldToGrid(position); + + auto it = positionToIndex.find(gridPos); + if (it == positionToIndex.end()) { + size_t index = positions.size(); + positions.push_back(gridPos); + colors.push_back(color); + layers.push_back(layer); + positionToIndex[gridPos] = index; + } else { + colors[it->second] = color; + layers[it->second] = layer; + } + } + + Vec4 getVoxel(const Vec3& position) const { + Vec3 gridPos = worldToGrid(position); + auto it = positionToIndex.find(gridPos); + if (it != positionToIndex.end()) { + return colors[it->second]; + } + return Vec4(0, 0, 0, 0); + } + + int getVoxelLayer(const Vec3& position) const { + Vec3 gridPos = worldToGrid(position); + auto it = positionToIndex.find(gridPos); + if (it != positionToIndex.end()) { + return layers[it->second]; + } + return EMPTY; + } + + bool isOccupied(const Vec3& position) const { + Vec3 gridPos = worldToGrid(position); + return positionToIndex.find(gridPos) != positionToIndex.end(); + } + + Vec3 worldToGrid(const Vec3& worldPos) const { + return (worldPos / voxelSize).floor(); + } + + Vec3 gridToWorld(const Vec3& gridPos) const { + return gridPos * voxelSize; + } + + const std::vector& getOccupiedPositions() const { + return positions; + } + + const std::vector& getColors() const { + return colors; + } + + const std::vector& getLayers() const { + return layers; + } + + const std::unordered_map& getPositionToIndexMap() const { + return positionToIndex; + } + + const Vec3& getGridSize() const { + return gridSize; + } + + const Vec3& getVoxelSize() const { + return voxelSize; + } + + void clear() { + positions.clear(); + colors.clear(); + layers.clear(); + positionToIndex.clear(); + } + + void assignPlanetaryLayers(const Vec3& center = Vec3(0, 0, 0)) { + TIME_FUNCTION; + printf("Assigning planetary layers...\n"); + + const float atmospherePercent = 0.05f; + const float crustPercent = 0.01f; + const float mantlePercent = 0.10f; + const float outerCorePercent = 0.42f; + const float innerCorePercent = 0.42f; + + float maxDistance = 0.0f; + for (const auto& pos : positions) { + Vec3 worldPos = gridToWorld(pos); + float distance = (worldPos - center).length(); + maxDistance = std::max(maxDistance, distance); + } + + printf("Maximum distance from center: %.2f\n", maxDistance); + + const float atmosphereStart = maxDistance * (1.0f - atmospherePercent); + const float crustStart = maxDistance * (1.0f - atmospherePercent - crustPercent); + const float mantleStart = maxDistance * (1.0f - atmospherePercent - crustPercent - mantlePercent); + const float outerCoreStart = maxDistance * (1.0f - atmospherePercent - crustPercent - mantlePercent - outerCorePercent); + + printf("Layer boundaries:\n"); + printf(" Atmosphere: %.2f to %.2f\n", atmosphereStart, maxDistance); + printf(" Crust: %.2f to %.2f\n", crustStart, atmosphereStart); + printf(" Mantle: %.2f to %.2f\n", mantleStart, crustStart); + printf(" Outer Core: %.2f to %.2f\n", outerCoreStart, mantleStart); + printf(" Inner Core: 0.00 to %.2f\n", outerCoreStart); + + int atmosphereCount = 0, crustCount = 0, mantleCount = 0, outerCoreCount = 0, innerCoreCount = 0; + + for (size_t i = 0; i < positions.size(); ++i) { + Vec3 worldPos = gridToWorld(positions[i]); + float distance = (worldPos - center).length(); + + Vec4 layerColor; + int layerType; + + if (distance >= atmosphereStart) { + // Atmosphere - transparent blue + layerColor = Vec4(0.2f, 0.4f, 1.0f, 0.3f); // Semi-transparent blue + layerType = ATMOSPHERE; + atmosphereCount++; + } else if (distance >= crustStart) { + // Crust - light brown + layerColor = Vec4(0.8f, 0.7f, 0.5f, 1.0f); // Light brown + layerType = CRUST; + crustCount++; + } else if (distance >= mantleStart) { + // Mantle - reddish brown + layerColor = Vec4(0.7f, 0.3f, 0.2f, 1.0f); // Reddish brown + layerType = MANTLE; + mantleCount++; + } else if (distance >= outerCoreStart) { + // Outer Core - orange/yellow + layerColor = Vec4(1.0f, 0.6f, 0.2f, 1.0f); // Orange + layerType = OUTER_CORE; + outerCoreCount++; + } else { + // Inner Core - bright yellow + layerColor = Vec4(1.0f, 0.9f, 0.1f, 1.0f); // Bright yellow + layerType = INNER_CORE; + innerCoreCount++; + } + + colors[i] = layerColor; + layers[i] = layerType; + } + + printf("Layer distribution:\n"); + printf(" Atmosphere: %d voxels (%.1f%%)\n", atmosphereCount, (atmosphereCount * 100.0f) / positions.size()); + printf(" Crust: %d voxels (%.1f%%)\n", crustCount, (crustCount * 100.0f) / positions.size()); + printf(" Mantle: %d voxels (%.1f%%)\n", mantleCount, (mantleCount * 100.0f) / positions.size()); + printf(" Outer Core: %d voxels (%.1f%%)\n", outerCoreCount, (outerCoreCount * 100.0f) / positions.size()); + printf(" Inner Core: %d voxels (%.1f%%)\n", innerCoreCount, (innerCoreCount * 100.0f) / positions.size()); + } +}; + +#endif \ No newline at end of file