From bff1efb2910d40132cc59fcda47b4b635f1cda6d Mon Sep 17 00:00:00 2001 From: Yggdrasil75 Date: Tue, 11 Nov 2025 14:34:19 -0500 Subject: [PATCH] moved stuff around, added a grayscale test. --- .vscode/settings.json | 3 +- tests/g2chromatic.cpp | 141 +++++++++++++ tests/g2grayscale.cpp | 74 +++++++ util/grid/grid2.hpp | 125 ++++++++---- util/grid/grid3.hpp | 340 ++++++++++++++++++------------- util/grid2.hpp | 231 --------------------- util/output/aviwriter.hpp | 349 ++++++++++++++++++++++++++++++++ util/{ => output}/bmpwriter.hpp | 2 +- util/{ => output}/jxlwriter.hpp | 0 util/{ => vectorlogic}/vec.cpp | 0 util/{ => vectorlogic}/vec2.hpp | 0 util/{ => vectorlogic}/vec3.hpp | 0 util/{ => vectorlogic}/vec4.hpp | 0 13 files changed, 851 insertions(+), 414 deletions(-) create mode 100644 tests/g2chromatic.cpp create mode 100644 tests/g2grayscale.cpp delete mode 100644 util/grid2.hpp create mode 100644 util/output/aviwriter.hpp rename util/{ => output}/bmpwriter.hpp (99%) rename util/{ => output}/jxlwriter.hpp (100%) rename util/{ => vectorlogic}/vec.cpp (100%) rename util/{ => vectorlogic}/vec2.hpp (100%) rename util/{ => vectorlogic}/vec3.hpp (100%) rename util/{ => vectorlogic}/vec4.hpp (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index acaa510..64836b7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -85,7 +85,8 @@ "cinttypes": "cpp", "variant": "cpp", "__nullptr": "cpp", - "unordered_set": "cpp" + "unordered_set": "cpp", + "queue": "cpp" }, "files.exclude": { "**/*.rpyc": true, diff --git a/tests/g2chromatic.cpp b/tests/g2chromatic.cpp new file mode 100644 index 0000000..43d1f9a --- /dev/null +++ b/tests/g2chromatic.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include "../util/grid/grid2.hpp" +#include "../util/output/aviwriter.hpp" + +int main() { + // Create a Grid2 instance + Grid2 grid; + + // Grid dimensions + const int width = 100; + const int height = 100; + const int totalFrames = 60; // 2 seconds at 30fps + + std::cout << "Creating chromatic transformation animation..." << std::endl; + + // Initialize with grayscale gradient + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + float gradient = (x + y) / float(width + height - 2); + Vec2 position(static_cast(x), static_cast(y)); + Vec4 color(gradient, gradient, gradient, 1.0f); + grid.addObject(position, color, 1.0f); + } + } + + std::cout << "Initial grayscale grid created with " << width * height << " objects" << std::endl; + + // Random number generation for seed points + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> xDist(0, width - 1); + std::uniform_int_distribution<> yDist(0, height - 1); + std::uniform_real_distribution<> colorDist(0.2f, 0.8f); + + // Generate multiple seed points for more interesting patterns + const int numSeeds = 8; + std::vector seedPoints; + std::vector seedColors; + + for (int i = 0; i < numSeeds; ++i) { + seedPoints.emplace_back(xDist(gen), yDist(gen)); + seedColors.emplace_back(colorDist(gen), colorDist(gen), colorDist(gen), colorDist(gen)); + } + + std::cout << "Generated " << numSeeds << " seed points for color propagation" << std::endl; + + // Create frames for AVI + std::vector> frames; + + for (int frame = 0; frame < totalFrames; ++frame) { + std::cout << "Processing frame " << frame + 1 << "/" << totalFrames << std::endl; + + // Apply color propagation based on frame progress + float progress = static_cast(frame) / (totalFrames - 1); + + // Update colors based on seed propagation + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + Vec2 currentPos(x, y); + size_t id = grid.getIndicesAt(currentPos)[0]; // Assuming one object per position + Vec4 originalColor = grid.getColor(id); + + Vec4 newColor = originalColor; + + // For each seed point, calculate influence + for (int s = 0; s < numSeeds; ++s) { + float distance = currentPos.distance(seedPoints[s]); + float maxDistance = std::max(width, height) * 0.6f; + float influence = std::max(0.0f, 1.0f - (distance / maxDistance)); + + // Apply influence based on relative position to seed + Vec2 direction = currentPos - seedPoints[s]; + float angle = std::atan2(direction.y, direction.x); + + // Different color channels respond to different directions + if (std::abs(angle) < M_PI / 4.0f) { // Right - affect alpha + newColor.a = std::fmod(newColor.a + seedColors[s].a * influence * progress, 1.0f); + } else if (std::abs(angle) > 3.0f * M_PI / 4.0f) { // Left - affect blue + newColor.b = std::fmod(newColor.b + seedColors[s].b * influence * progress, 1.0f); + } else if (angle > 0) { // Below - affect green + newColor.g = std::fmod(newColor.g + seedColors[s].g * influence * progress, 1.0f); + } else { // Above - affect red + newColor.r = std::fmod(newColor.r + seedColors[s].r * influence * progress, 1.0f); + } + } + + // Clamp colors to valid range + newColor = newColor.clampColor(); + grid.setColor(id, newColor); + } + } + + // Get current frame as RGB data + int frameWidth, frameHeight; + std::vector rgbData; + grid.getGridAsRGB(frameWidth, frameHeight, rgbData); + + // Convert to BGR format for AVI (OpenCV uses BGR) + std::vector bgrFrame(frameWidth * frameHeight * 3); + #pragma omp parallel for + for (int i = 0; i < frameWidth * frameHeight; ++i) { + bgrFrame[i * 3] = rgbData[i * 3 + 2]; + bgrFrame[i * 3 + 1] = rgbData[i * 3 + 1]; + bgrFrame[i * 3 + 2] = rgbData[i * 3]; + } + // for (int i = 0; i < frameWidth * frameHeight; ++i) { + // bgrFrame[i * 3] = static_cast(rgbData[i * 3 + 2]); // B + // bgrFrame[i * 3 + 1] = static_cast(rgbData[i * 3 + 1]); // G + // bgrFrame[i * 3 + 2] = static_cast(rgbData[i * 3]); // R + // } + + frames.push_back(bgrFrame); + } + + // Save as AVI + std::string filename = "output/chromatic_transformation.avi"; + bool success = AVIWriter::saveAVI(filename, frames, width, height, 30.0f); + + if (success) { + std::cout << "\nSuccessfully saved chromatic transformation animation to: " << filename << std::endl; + std::cout << "Video details:" << std::endl; + std::cout << " - Dimensions: " << width << " x " << height << std::endl; + std::cout << " - Frames: " << totalFrames << " (2 seconds at 30fps)" << std::endl; + std::cout << " - Seed points: " << numSeeds << std::endl; + + // Print seed point information + std::cout << "\nSeed points used:" << std::endl; + for (int i = 0; i < numSeeds; ++i) { + std::cout << " Seed " << i + 1 << ": Position " << seedPoints[i] + << ", Color " << seedColors[i].toColorString() << std::endl; + } + } else { + std::cerr << "Failed to save AVI file!" << std::endl; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/tests/g2grayscale.cpp b/tests/g2grayscale.cpp new file mode 100644 index 0000000..746c473 --- /dev/null +++ b/tests/g2grayscale.cpp @@ -0,0 +1,74 @@ +#include +#include +#include "../util/grid/grid2.hpp" +#include "../util/output/bmpwriter.hpp" + +int main() { + // Create a Grid2 instance + Grid2 grid; + + // Grid dimensions + const int width = 100; + const int height = 100; + + std::cout << "Creating grayscale gradient..." << std::endl; + + // Add objects to create a grayscale gradient + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + // Calculate gradient value (0.0 at top-left to 1.0 at bottom-right) + float gradient = (x + y) / float(width + height - 2); + + // Create position + Vec2 position(static_cast(x), static_cast(y)); + + // Create grayscale color (r=g=b=gradient, a=1.0) + Vec4 color(gradient, gradient, gradient, 1.0f); + + // Add to grid with size 1.0 (single pixel) + grid.addObject(position, color, 1.0f); + } + } + + std::cout << "Added " << width * height << " objects to grid" << std::endl; + + // Get the entire grid as RGB data + int outputWidth, outputHeight; + std::vector rgbData; + grid.getGridAsRGB(outputWidth, outputHeight, rgbData); + + std::cout << "Output dimensions: " << outputWidth << " x " << outputHeight << std::endl; + std::cout << "RGB data size: " << rgbData.size() << " elements" << std::endl; + + // Convert RGB data to format suitable for BMPWriter + std::vector pixels; + pixels.reserve(outputWidth * outputHeight); + + for (size_t i = 0; i < rgbData.size(); i += 3) { + float r = rgbData[i] / 255.0f; + float g = rgbData[i + 1] / 255.0f; + float b = rgbData[i + 2] / 255.0f; + pixels.emplace_back(r, g, b); + } + + // Save as BMP + std::string filename = "output/grayscale_gradient.bmp"; + bool success = BMPWriter::saveBMP(filename, pixels, outputWidth, outputHeight); + + if (success) { + std::cout << "Successfully saved grayscale gradient to: " << filename << std::endl; + + // Print some gradient values for verification + std::cout << "\nGradient values at key positions:" << std::endl; + std::cout << "Top-left (0,0): " << grid.getColor(grid.getIndicesAt(0, 0)[0]).r << std::endl; + std::cout << "Center (" << width/2 << "," << height/2 << "): " + << grid.getColor(grid.getIndicesAt(width/2, height/2)[0]).r << std::endl; + std::cout << "Bottom-right (" << width-1 << "," << height-1 << "): " + << grid.getColor(grid.getIndicesAt(width-1, height-1)[0]).r << std::endl; + } else { + std::cerr << "Failed to save BMP file!" << std::endl; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/util/grid/grid2.hpp b/util/grid/grid2.hpp index 580be74..537f5a8 100644 --- a/util/grid/grid2.hpp +++ b/util/grid/grid2.hpp @@ -1,14 +1,24 @@ #ifndef GRID2_HPP #define GRID2_HPP -#include "../vec2.hpp" -#include "../vec4.hpp" +#include "../vectorlogic/vec2.hpp" +#include "../vectorlogic/vec4.hpp" #include #include #include #include #include #include +#include + +struct PairHash { + template + std::size_t operator()(const std::pair& p) const { + auto h1 = std::hash{}(p.first); + auto h2 = std::hash{}(p.second); + return h1 ^ (h2 << 1); + } +}; class Grid2 { private: @@ -23,12 +33,12 @@ private: size_t next_id; std::unordered_map> cellIndices; // object ID -> grid cell - std::unordered_map, std::unordered_set> spatialGrid; // cell -> object IDs + std::unordered_map, std::unordered_set, PairHash> spatialGrid; // cell -> object IDs float cellSize; public: Grid2() : next_id(0), cellSize(1.0f) {} - Grid2(float cellSize = 1.0f) : next_id(0), cellSize(cellSize) {} + Grid2(float cellSize) : next_id(0), cellSize(cellSize) {} size_t addObject(const Vec2& position, const Vec4& color, float size = 1.0f) { size_t id = next_id++; @@ -43,19 +53,19 @@ public: //gets Vec2 getPosition(size_t id) const { - auto it = positions.find(id); + std::multimap::const_iterator it = positions.find(id); if (it != positions.end()) return it->second; return Vec2(); } Vec4 getColor(size_t id) const { - auto it = colors.find(id); + std::multimap::const_iterator it = colors.find(id); if (it != colors.end()) return it->second; return Vec4(); } float getSize(size_t id) const { - auto it = sizes.find(id); + std::multimap::const_iterator it = sizes.find(id); if (it != sizes.end()) return it->second; return 1.0f; } @@ -82,7 +92,7 @@ public: // Batch add/remove operations void addObjects(const std::vector>& objects) { - for (const auto& obj : objects) { + for (const std::tuple& obj : objects) { addObject(std::get<0>(obj), std::get<1>(obj), std::get<2>(obj)); } } @@ -98,7 +108,7 @@ public: // Bulk update spatial grid - collect all changes first std::vector> spatialUpdates; - for (const auto& pair : newPositions) { + for (const std::pair& pair : newPositions) { if (hasObject(pair.first)) { Vec2 oldPos = getPosition(pair.first); positions.erase(pair.first); @@ -108,7 +118,7 @@ public: } // Apply all spatial updates at once - for (const auto& update : spatialUpdates) { + for (const std::tuple& update : spatialUpdates) { updateSpatialIndex(std::get<0>(update), std::get<1>(update), std::get<2>(update)); } } @@ -178,10 +188,13 @@ public: for (const auto& pair : positions) { const Vec2& pos = pair.second; - minCorner.x = std::min(minCorner.x, pos.x); - minCorner.y = std::min(minCorner.y, pos.y); - maxCorner.x = std::max(maxCorner.x, pos.x); - maxCorner.y = std::max(maxCorner.y, pos.y); + float size = getSize(pair.first); + float halfSize = size * 0.5f; + + minCorner.x = std::min(minCorner.x, pos.x - halfSize); + minCorner.y = std::min(minCorner.y, pos.y - halfSize); + maxCorner.x = std::max(maxCorner.x, pos.x + halfSize); + maxCorner.y = std::max(maxCorner.y, pos.y + halfSize); } } @@ -197,23 +210,36 @@ public: // Initialize with black (0,0,0) rgbData.resize(width * height * 3, 0); - // Fill the grid with object colors + // Fill the grid with object colors, accounting for sizes for (const auto& posPair : positions) { size_t id = posPair.first; const Vec2& pos = posPair.second; + float size = getSize(id); + const Vec4& color = getColor(id); - // Convert world position to grid coordinates - int gridX = static_cast(pos.x - minCorner.x); - int gridY = static_cast(pos.y - minCorner.y); + // Calculate the bounding box of this object in grid coordinates + float halfSize = size * 0.5f; + int minGridX = static_cast(std::floor((pos.x - halfSize - minCorner.x))); + int minGridY = static_cast(std::floor((pos.y - halfSize - minCorner.y))); + int maxGridX = static_cast(std::ceil((pos.x + halfSize - minCorner.x))); + int maxGridY = static_cast(std::ceil((pos.y + halfSize - minCorner.y))); - if (gridX >= 0 && gridX < width && gridY >= 0 && gridY < height) { - const Vec4& color = getColor(id); - int index = (gridY * width + gridX) * 3; - - // Convert float color [0,1] to int [0,255] - rgbData[index] = static_cast(color.r * 255); - rgbData[index + 1] = static_cast(color.g * 255); - rgbData[index + 2] = static_cast(color.b * 255); + // Clamp to grid boundaries + minGridX = std::max(0, minGridX); + minGridY = std::max(0, minGridY); + maxGridX = std::min(width - 1, maxGridX); + maxGridY = std::min(height - 1, maxGridY); + + // Fill all pixels within the object's size + for (int y = minGridY; y <= maxGridY; ++y) { + for (int x = minGridX; x <= maxGridX; ++x) { + int index = (y * width + x) * 3; + + // Convert float color [0,1] to int [0,255] + rgbData[index] = static_cast(color.r * 255); + rgbData[index + 1] = static_cast(color.g * 255); + rgbData[index + 2] = static_cast(color.b * 255); + } } } } @@ -235,25 +261,44 @@ public: // Initialize with black (0,0,0) rgbData.resize(width * height * 3, 0); - // Fill the grid with object colors in the region + // Fill the grid with object colors in the region, accounting for sizes for (const auto& posPair : positions) { size_t id = posPair.first; const Vec2& pos = posPair.second; + float size = getSize(id); + const Vec4& color = getColor(id); - // Check if position is within the region - if (pos.x >= minX && pos.x < maxX && pos.y >= minY && pos.y < maxY) { - // Convert world position to grid coordinates - int gridX = static_cast(pos.x - minX); - int gridY = static_cast(pos.y - minY); + // Calculate the bounding box of this object in world coordinates + float halfSize = size * 0.5f; + float objMinX = pos.x - halfSize; + float objMinY = pos.y - halfSize; + float objMaxX = pos.x + halfSize; + float objMaxY = pos.y + halfSize; + + // Check if object overlaps with the region + if (objMaxX >= minX && objMinX <= maxX && objMaxY >= minY && objMinY <= maxY) { + // Calculate overlapping region in grid coordinates + int minGridX = static_cast(std::floor(std::max(objMinX, minX) - minX)); + int minGridY = static_cast(std::floor(std::max(objMinY, minY) - minY)); + int maxGridX = static_cast(std::ceil(std::min(objMaxX, maxX) - minX)); + int maxGridY = static_cast(std::ceil(std::min(objMaxY, maxY) - minY)); - if (gridX >= 0 && gridX < width && gridY >= 0 && gridY < height) { - const Vec4& color = getColor(id); - int index = (gridY * width + gridX) * 3; - - // Convert float color [0,1] to int [0,255] - rgbData[index] = static_cast(color.r * 255); - rgbData[index + 1] = static_cast(color.g * 255); - rgbData[index + 2] = static_cast(color.b * 255); + // Clamp to grid boundaries + minGridX = std::max(0, minGridX); + minGridY = std::max(0, minGridY); + maxGridX = std::min(width - 1, maxGridX); + maxGridY = std::min(height - 1, maxGridY); + + // Fill all pixels within the object's overlapping region + for (int y = minGridY; y <= maxGridY; ++y) { + for (int x = minGridX; x <= maxGridX; ++x) { + int index = (y * width + x) * 3; + + // Convert float color [0,1] to int [0,255] + rgbData[index] = static_cast(color.r * 255); + rgbData[index + 1] = static_cast(color.g * 255); + rgbData[index + 2] = static_cast(color.b * 255); + } } } } diff --git a/util/grid/grid3.hpp b/util/grid/grid3.hpp index 8b49ddb..dcde2b2 100644 --- a/util/grid/grid3.hpp +++ b/util/grid/grid3.hpp @@ -1,28 +1,26 @@ #ifndef GRID3_HPP #define GRID3_HPP -#include "vec3.hpp" -#include "vec4.hpp" +#include "../vectorlogic/vec3.hpp" +#include "../vectorlogic/vec4.hpp" +#include "grid2.hpp" #include #include #include #include #include #include +#include class Grid3 { private: - // size_t is index - // Vec3 is x,y,z position of the sparse voxel std::multimap positions; - // Vec4 is rgba color at the position std::multimap colors; - // size is a floating size to assign to a voxel to allow larger or smaller assignments std::multimap sizes; size_t next_id; - std::unordered_map> cellIndices; // object ID -> grid cell - std::unordered_map, std::unordered_set> spatialGrid; // cell -> object IDs + std::unordered_map> cellIndices; + std::unordered_map, std::unordered_set> spatialGrid; float cellSize; public: @@ -40,7 +38,7 @@ public: return id; } - // Gets + // Get operations Vec3 getPosition(size_t id) const { auto it = positions.find(id); if (it != positions.end()) return it->second; @@ -59,7 +57,7 @@ public: return 1.0f; } - // Sets + // Set operations void setPosition(size_t id, const Vec3& position) { if (!hasObject(id)) return; @@ -79,7 +77,7 @@ public: sizes.insert({id, size}); } - // Batch add/remove operations + // Batch operations void addObjects(const std::vector>& objects) { for (const auto& obj : objects) { addObject(std::get<0>(obj), std::get<1>(obj), std::get<2>(obj)); @@ -92,9 +90,7 @@ public: } } - // Batch position updates void updatePositions(const std::unordered_map& newPositions) { - // Bulk update spatial grid - collect all changes first std::vector> spatialUpdates; for (const auto& pair : newPositions) { @@ -106,19 +102,18 @@ public: } } - // Apply all spatial updates at once for (const auto& update : spatialUpdates) { updateSpatialIndex(std::get<0>(update), std::get<1>(update), std::get<2>(update)); } } - // Other + // Object management bool hasObject(size_t id) const { return positions.find(id) != positions.end(); } void removeObject(size_t id) { - // Remove from spatial grid first + // Remove from spatial grid auto cellIt = cellIndices.find(id); if (cellIt != cellIndices.end()) { auto& cellObjects = spatialGrid[cellIt->second]; @@ -135,6 +130,7 @@ public: sizes.erase(id); } + // Spatial queries std::vector getIndicesAt(float x, float y, float z, float radius = 0.0f) const { return getIndicesAt(Vec3(x, y, z), radius); } @@ -165,6 +161,7 @@ public: return result; } + // Bounding box void getBoundingBox(Vec3& minCorner, Vec3& maxCorner) const { if (positions.empty()) { minCorner = Vec3(0.0f, 0.0f, 0.0f); @@ -178,124 +175,216 @@ public: for (const auto& pair : positions) { const Vec3& pos = pair.second; - minCorner.x = std::min(minCorner.x, pos.x); - minCorner.y = std::min(minCorner.y, pos.y); - minCorner.z = std::min(minCorner.z, pos.z); - maxCorner.x = std::max(maxCorner.x, pos.x); - maxCorner.y = std::max(maxCorner.y, pos.y); - maxCorner.z = std::max(maxCorner.z, pos.z); + float size = getSize(pair.first); + float halfSize = size * 0.5f; + + minCorner.x = std::min(minCorner.x, pos.x - halfSize); + minCorner.y = std::min(minCorner.y, pos.y - halfSize); + minCorner.z = std::min(minCorner.z, pos.z - halfSize); + maxCorner.x = std::max(maxCorner.x, pos.x + halfSize); + maxCorner.y = std::max(maxCorner.y, pos.y + halfSize); + maxCorner.z = std::max(maxCorner.z, pos.z + halfSize); } } - // Get 2D slice of the 3D grid (useful for visualization) - void getSliceAsRGB(int axis, float slicePos, - int& width, int& height, std::vector& rgbData) const { + // Grid2 slice generation + Grid2 getSliceXY(float z, float thickness = 0.1f) const { + Grid2 slice; Vec3 minCorner, maxCorner; getBoundingBox(minCorner, maxCorner); - // Determine slice dimensions based on axis (0=x, 1=y, 2=z) - if (axis == 0) { // X-slice - width = static_cast(std::ceil(maxCorner.z - minCorner.z)) + 1; - height = static_cast(std::ceil(maxCorner.y - minCorner.y)) + 1; - } else if (axis == 1) { // Y-slice - width = static_cast(std::ceil(maxCorner.z - minCorner.z)) + 1; - height = static_cast(std::ceil(maxCorner.x - minCorner.x)) + 1; - } else { // Z-slice - width = static_cast(std::ceil(maxCorner.x - minCorner.x)) + 1; - height = static_cast(std::ceil(maxCorner.y - minCorner.y)) + 1; - } + float halfThickness = thickness * 0.5f; + float minZ = z - halfThickness; + float maxZ = z + halfThickness; - // Initialize with black (0,0,0) - rgbData.resize(width * height * 3, 0); - - // Fill the slice with object colors for (const auto& posPair : positions) { size_t id = posPair.first; const Vec3& pos = posPair.second; - // Check if position is within slice tolerance - float tolerance = 0.5f; // Half voxel tolerance - bool inSlice = false; - int gridX = 0, gridY = 0; - - if (axis == 0 && std::abs(pos.x - slicePos) <= tolerance) { // X-slice - gridX = static_cast(pos.z - minCorner.z); - gridY = static_cast(pos.y - minCorner.y); - inSlice = true; - } else if (axis == 1 && std::abs(pos.y - slicePos) <= tolerance) { // Y-slice - gridX = static_cast(pos.z - minCorner.z); - gridY = static_cast(pos.x - minCorner.x); - inSlice = true; - } else if (axis == 2 && std::abs(pos.z - slicePos) <= tolerance) { // Z-slice - gridX = static_cast(pos.x - minCorner.x); - gridY = static_cast(pos.y - minCorner.y); - inSlice = true; - } - - if (inSlice && gridX >= 0 && gridX < width && gridY >= 0 && gridY < height) { - const Vec4& color = getColor(id); - int index = (gridY * width + gridX) * 3; - - // Convert float color [0,1] to int [0,255] - rgbData[index] = static_cast(color.r * 255); - rgbData[index + 1] = static_cast(color.g * 255); - rgbData[index + 2] = static_cast(color.b * 255); + if (pos.z >= minZ && pos.z <= maxZ) { + // Project to XY plane + Vec2 slicePos(pos.x, pos.y); + slice.addObject(slicePos, getColor(id), getSize(id)); } } + + return slice; } - void getRegionAsRGB(float minX, float minY, float minZ, float maxX, float maxY, float maxZ, - int& width, int& height, std::vector& rgbData) const { - // For 3D, this creates a 2D projection (XY plane at average Z) - if (minX >= maxX || minY >= maxY || minZ >= maxZ) { - width = 0; - height = 0; - rgbData.clear(); - return; - } + Grid2 getSliceXZ(float y, float thickness = 0.1f) const { + Grid2 slice; + Vec3 minCorner, maxCorner; + getBoundingBox(minCorner, maxCorner); - // Calculate grid dimensions for XY projection - width = static_cast(std::ceil(maxX - minX)); - height = static_cast(std::ceil(maxY - minY)); + float halfThickness = thickness * 0.5f; + float minY = y - halfThickness; + float maxY = y + halfThickness; - // Initialize with black (0,0,0) - rgbData.resize(width * height * 3, 0); - - // Fill the grid with object colors in the region (XY projection) for (const auto& posPair : positions) { size_t id = posPair.first; const Vec3& pos = posPair.second; - // Check if position is within the region - if (pos.x >= minX && pos.x < maxX && - pos.y >= minY && pos.y < maxY && - pos.z >= minZ && pos.z < maxZ) { - - // Convert world position to grid coordinates (XY projection) - int gridX = static_cast(pos.x - minX); - int gridY = static_cast(pos.y - minY); - - if (gridX >= 0 && gridX < width && gridY >= 0 && gridY < height) { - const Vec4& color = getColor(id); - int index = (gridY * width + gridX) * 3; + if (pos.y >= minY && pos.y <= maxY) { + // Project to XZ plane + Vec2 slicePos(pos.x, pos.z); + slice.addObject(slicePos, getColor(id), getSize(id)); + } + } + + return slice; + } + + Grid2 getSliceYZ(float x, float thickness = 0.1f) const { + Grid2 slice; + Vec3 minCorner, maxCorner; + getBoundingBox(minCorner, maxCorner); + + float halfThickness = thickness * 0.5f; + float minX = x - halfThickness; + float maxX = x + halfThickness; + + for (const auto& posPair : positions) { + size_t id = posPair.first; + const Vec3& pos = posPair.second; + + if (pos.x >= minX && pos.x <= maxX) { + // Project to YZ plane + Vec2 slicePos(pos.y, pos.z); + slice.addObject(slicePos, getColor(id), getSize(id)); + } + } + + return slice; + } + + // Amanatides and Woo ray-grid intersection + struct RayHit { + size_t objectId; + Vec3 position; + Vec3 normal; + float distance; + Vec4 color; + + RayHit() : objectId(-1), distance(std::numeric_limits::max()) {} + }; + + RayHit amanatidesWooRaycast(const Vec3& rayOrigin, const Vec3& rayDirection, float maxDistance = 1000.0f) const { + RayHit hit; + + if (positions.empty()) return hit; + + // Normalize direction + Vec3 dir = rayDirection.normalized(); + + // Initialize grid traversal + auto startCell = worldToGrid(rayOrigin); + int cellX = std::get<0>(startCell); + int cellY = std::get<1>(startCell); + int cellZ = std::get<2>(startCell); + + // Step directions + int stepX = (dir.x > 0) ? 1 : -1; + int stepY = (dir.y > 0) ? 1 : -1; + int stepZ = (dir.z > 0) ? 1 : -1; + + // Calculate cell boundaries + float cellMinX = cellX * cellSize; + float cellMinY = cellY * cellSize; + float cellMinZ = cellZ * cellSize; + float cellMaxX = cellMinX + cellSize; + float cellMaxY = cellMinY + cellSize; + float cellMaxZ = cellMinZ + cellSize; + + // Calculate t values for cell boundaries + float tMaxX, tMaxY, tMaxZ; + if (dir.x != 0) { + tMaxX = ((dir.x > 0 ? cellMaxX : cellMinX) - rayOrigin.x) / dir.x; + } else { + tMaxX = std::numeric_limits::max(); + } + + if (dir.y != 0) { + tMaxY = ((dir.y > 0 ? cellMaxY : cellMinY) - rayOrigin.y) / dir.y; + } else { + tMaxY = std::numeric_limits::max(); + } + + if (dir.z != 0) { + tMaxZ = ((dir.z > 0 ? cellMaxZ : cellMinZ) - rayOrigin.z) / dir.z; + } else { + tMaxZ = std::numeric_limits::max(); + } + + // Calculate t delta + float tDeltaX = (cellSize / std::abs(dir.x)) * (dir.x != 0 ? 1 : 0); + float tDeltaY = (cellSize / std::abs(dir.y)) * (dir.y != 0 ? 1 : 0); + float tDeltaZ = (cellSize / std::abs(dir.z)) * (dir.z != 0 ? 1 : 0); + + // Traverse grid + float t = 0.0f; + while (t < maxDistance) { + // Check current cell for intersections + auto cell = std::make_tuple(cellX, cellY, cellZ); + auto cellIt = spatialGrid.find(cell); + if (cellIt != spatialGrid.end()) { + // Check all objects in this cell + for (size_t id : cellIt->second) { + const Vec3& objPos = getPosition(id); + float objSize = getSize(id); - // Convert float color [0,1] to int [0,255] - rgbData[index] = static_cast(color.r * 255); - rgbData[index + 1] = static_cast(color.g * 255); - rgbData[index + 2] = static_cast(color.b * 255); + // Simple sphere intersection test + Vec3 toObj = objPos - rayOrigin; + float b = toObj.dot(dir); + float c = toObj.dot(toObj) - objSize * objSize; + float discriminant = b * b - c; + + if (discriminant >= 0) { + float sqrtDisc = std::sqrt(discriminant); + float t1 = b - sqrtDisc; + float t2 = b + sqrtDisc; + + if (t1 >= 0 && t1 < hit.distance) { + hit.objectId = id; + hit.position = rayOrigin + dir * t1; + hit.normal = (hit.position - objPos).normalized(); + hit.distance = t1; + hit.color = getColor(id); + } else if (t2 >= 0 && t2 < hit.distance) { + hit.objectId = id; + hit.position = rayOrigin + dir * t2; + hit.normal = (hit.position - objPos).normalized(); + hit.distance = t2; + hit.color = getColor(id); + } + } + } + + // If we found a hit, return it + if (hit.objectId != static_cast(-1)) { + return hit; } } + + // Move to next cell + if (tMaxX < tMaxY && tMaxX < tMaxZ) { + cellX += stepX; + t = tMaxX; + tMaxX += tDeltaX; + } else if (tMaxY < tMaxZ) { + cellY += stepY; + t = tMaxY; + tMaxY += tDeltaY; + } else { + cellZ += stepZ; + t = tMaxZ; + tMaxZ += tDeltaZ; + } } + + return hit; } - void getRegionAsRGB(const Vec3& minCorner, const Vec3& maxCorner, - int& width, int& height, std::vector& rgbData) const { - getRegionAsRGB(minCorner.x, minCorner.y, minCorner.z, - maxCorner.x, maxCorner.y, maxCorner.z, - width, height, rgbData); - } - - // Spatial grid methods for 3D + // Spatial indexing std::tuple worldToGrid(const Vec3& pos) const { return { static_cast(std::floor(pos.x / cellSize)), @@ -335,7 +424,7 @@ public: float radiusSq = radius * radius; - // Only check relevant cells + // Check relevant cells for (int x = std::get<0>(minCell); x <= std::get<0>(maxCell); ++x) { for (int y = std::get<1>(minCell); y <= std::get<1>(maxCell); ++y) { for (int z = std::get<2>(minCell); z <= std::get<2>(maxCell); ++z) { @@ -387,42 +476,11 @@ public: return result; } + // Statistics + size_t getObjectCount() const { return positions.size(); } size_t getSpatialGridCellCount() const { return spatialGrid.size(); } size_t getSpatialGridObjectCount() const { return cellIndices.size(); } float getCellSize() const { return cellSize; } - - // 3D-specific utility methods - size_t getVoxelCount() const { return positions.size(); } - - // Get density information (useful for volume rendering) - std::vector getDensityGrid(int resX, int resY, int resZ) const { - std::vector density(resX * resY * resZ, 0.0f); - - Vec3 minCorner, maxCorner; - getBoundingBox(minCorner, maxCorner); - - Vec3 gridSize = maxCorner - minCorner; - if (gridSize.x <= 0 || gridSize.y <= 0 || gridSize.z <= 0) { - return density; - } - - Vec3 voxelSize(gridSize.x / resX, gridSize.y / resY, gridSize.z / resZ); - - for (const auto& posPair : positions) { - const Vec3& pos = posPair.second; - - // Convert to grid coordinates - int gx = static_cast((pos.x - minCorner.x) / gridSize.x * resX); - int gy = static_cast((pos.y - minCorner.y) / gridSize.y * resY); - int gz = static_cast((pos.z - minCorner.z) / gridSize.z * resZ); - - if (gx >= 0 && gx < resX && gy >= 0 && gy < resY && gz >= 0 && gz < resZ) { - density[gz * resX * resY + gy * resX + gx] += 1.0f; - } - } - - return density; - } }; #endif \ No newline at end of file diff --git a/util/grid2.hpp b/util/grid2.hpp deleted file mode 100644 index 9077aeb..0000000 --- a/util/grid2.hpp +++ /dev/null @@ -1,231 +0,0 @@ -#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 diff --git a/util/output/aviwriter.hpp b/util/output/aviwriter.hpp new file mode 100644 index 0000000..72c2fae --- /dev/null +++ b/util/output/aviwriter.hpp @@ -0,0 +1,349 @@ +#ifndef AVI_WRITER_HPP +#define AVI_WRITER_HPP + +#include +#include +#include +#include +#include +#include +#include + +class AVIWriter { +private: + #pragma pack(push, 1) + struct RIFFChunk { + uint32_t chunkId; + uint32_t chunkSize; + uint32_t format; + }; + + struct AVIListHeader { + uint32_t listId; + uint32_t listSize; + uint32_t listType; + }; + + struct AVIMainHeader { + uint32_t microSecPerFrame; + uint32_t maxBytesPerSec; + uint32_t paddingGranularity; + uint32_t flags; + uint32_t totalFrames; + uint32_t initialFrames; + uint32_t streams; + uint32_t suggestedBufferSize; + uint32_t width; + uint32_t height; + uint32_t reserved[4]; + }; + + struct AVIStreamHeader { + uint32_t type; + uint32_t handler; + uint32_t flags; + uint16_t priority; + uint16_t language; + uint32_t initialFrames; + uint32_t scale; + uint32_t rate; + uint32_t start; + uint32_t length; + uint32_t suggestedBufferSize; + uint32_t quality; + uint32_t sampleSize; + struct { + int16_t left; + int16_t top; + int16_t right; + int16_t bottom; + } rcFrame; + }; + + struct BITMAPINFOHEADER { + uint32_t size; + int32_t width; + int32_t height; + uint16_t planes; + uint16_t bitCount; + uint32_t compression; + uint32_t sizeImage; + int32_t xPelsPerMeter; + int32_t yPelsPerMeter; + uint32_t clrUsed; + uint32_t clrImportant; + }; + + struct AVIIndexEntry { + uint32_t chunkId; + uint32_t flags; + uint32_t offset; + uint32_t size; + }; + #pragma pack(pop) + + static bool createDirectoryIfNeeded(const std::string& filename) { + std::filesystem::path filePath(filename); + std::filesystem::path directory = filePath.parent_path(); + + if (!directory.empty() && !std::filesystem::exists(directory)) { + return std::filesystem::create_directories(directory); + } + return true; + } + + static void writeChunk(std::ofstream& file, uint32_t chunkId, const void* data, uint32_t size) { + file.write(reinterpret_cast(&chunkId), 4); + file.write(reinterpret_cast(&size), 4); + if (data && size > 0) { + file.write(reinterpret_cast(data), size); + } + } + + static void writeList(std::ofstream& file, uint32_t listType, const void* data, uint32_t size) { + uint32_t listId = 0x5453494C; // 'LIST' + file.write(reinterpret_cast(&listId), 4); + file.write(reinterpret_cast(&size), 4); + file.write(reinterpret_cast(&listType), 4); + if (data && size > 4) { + file.write(reinterpret_cast(data), size - 4); + } + } + +public: + static bool saveAVI(const std::string& filename, + const std::vector>& frames, + int width, int height, float fps = 30.0f) { + if (frames.empty() || width <= 0 || height <= 0 || fps <= 0) { + return false; + } + + std::cout << "1" << "width: " << width << + "height: " << height << "frame count: " << fps << std::endl; + + // Validate frame sizes + size_t expectedFrameSize = width * height * 3; + for (const auto& frame : frames) { + if (frame.size() != expectedFrameSize) { + return false; + } + } + + std::cout << "2" << std::endl; + // Create directory if needed + if (!createDirectoryIfNeeded(filename)) { + return false; + } + + std::cout << "3" << std::endl; + std::ofstream file(filename, std::ios::binary); + if (!file) { + return false; + } + + uint32_t frameCount = static_cast(frames.size()); + uint32_t microSecPerFrame = static_cast(1000000.0f / fps); + + // Calculate padding for each frame (BMP-style row padding) + uint32_t rowSize = (width * 3 + 3) & ~3; + uint32_t frameSize = rowSize * height; + uint32_t totalDataSize = frameCount * frameSize; + + std::cout << "4" << std::endl; + // RIFF AVI header + RIFFChunk riffHeader; + riffHeader.chunkId = 0x46464952; // 'RIFF' + riffHeader.format = 0x20495641; // 'AVI ' + + // We'll come back and write the size at the end + uint32_t riffStartPos = static_cast(file.tellp()); + file.write(reinterpret_cast(&riffHeader), sizeof(riffHeader)); + + // hdrl list + uint32_t hdrlListStart = static_cast(file.tellp()); + writeList(file, 0x6C726468, nullptr, 0); // 'hdrl' - we'll fill size later + + std::cout << "5" << std::endl; + // avih chunk + AVIMainHeader mainHeader; + mainHeader.microSecPerFrame = microSecPerFrame; + mainHeader.maxBytesPerSec = frameSize * static_cast(fps); + mainHeader.paddingGranularity = 0; + mainHeader.flags = 0x000010; // HASINDEX flag + mainHeader.totalFrames = frameCount; + mainHeader.initialFrames = 0; + mainHeader.streams = 1; + mainHeader.suggestedBufferSize = frameSize; + mainHeader.width = width; + mainHeader.height = height; + mainHeader.reserved[0] = 0; + mainHeader.reserved[1] = 0; + mainHeader.reserved[2] = 0; + mainHeader.reserved[3] = 0; + + writeChunk(file, 0x68697661, &mainHeader, sizeof(mainHeader)); // 'avih' + + std::cout << "6" << std::endl; + // strl list + uint32_t strlListStart = static_cast(file.tellp()); + writeList(file, 0x6C727473, nullptr, 0); // 'strl' - we'll fill size later + + // strh chunk + AVIStreamHeader streamHeader; + streamHeader.type = 0x73646976; // 'vids' + streamHeader.handler = 0x00000000; // Uncompressed + streamHeader.flags = 0; + streamHeader.priority = 0; + streamHeader.language = 0; + streamHeader.initialFrames = 0; + streamHeader.scale = 1; + streamHeader.rate = static_cast(fps); + streamHeader.start = 0; + streamHeader.length = frameCount; + streamHeader.suggestedBufferSize = frameSize; + streamHeader.quality = 0xFFFFFFFF; // Default quality + streamHeader.sampleSize = 0; + streamHeader.rcFrame.left = 0; + streamHeader.rcFrame.top = 0; + streamHeader.rcFrame.right = width; + streamHeader.rcFrame.bottom = height; + + writeChunk(file, 0x68727473, &streamHeader, sizeof(streamHeader)); // 'strh' + + // strf chunk + BITMAPINFOHEADER bitmapInfo; + bitmapInfo.size = sizeof(BITMAPINFOHEADER); + bitmapInfo.width = width; + bitmapInfo.height = height; + bitmapInfo.planes = 1; + bitmapInfo.bitCount = 24; + bitmapInfo.compression = 0; // BI_RGB - uncompressed + bitmapInfo.sizeImage = frameSize; + bitmapInfo.xPelsPerMeter = 0; + bitmapInfo.yPelsPerMeter = 0; + bitmapInfo.clrUsed = 0; + bitmapInfo.clrImportant = 0; + + writeChunk(file, 0x66727473, &bitmapInfo, sizeof(bitmapInfo)); // 'strf' + + std::cout << "7" << std::endl; + // Update strl list size + uint32_t strlListEnd = static_cast(file.tellp()); + file.seekp(strlListStart + 4); + uint32_t strlListSize = strlListEnd - strlListStart - 8; + file.write(reinterpret_cast(&strlListSize), 4); + file.seekp(strlListEnd); + + std::cout << "8" << std::endl; + // Update hdrl list size + uint32_t hdrlListEnd = static_cast(file.tellp()); + file.seekp(hdrlListStart + 4); + uint32_t hdrlListSize = hdrlListEnd - hdrlListStart - 8; + file.write(reinterpret_cast(&hdrlListSize), 4); + file.seekp(hdrlListEnd); + + std::cout << "9" << std::endl; + // movi list + uint32_t moviListStart = static_cast(file.tellp()); + writeList(file, 0x69766F6D, nullptr, 0); // 'movi' - we'll fill size later + + std::vector indexEntries; + indexEntries.reserve(frameCount); + + // Write frames + for (uint32_t i = 0; i < frameCount; ++i) { + uint32_t frameStart = static_cast(file.tellp()) - moviListStart - 4; + + std::cout << "10-" << i << std::endl; + // Create padded frame data (BMP-style bottom-to-top with padding) + std::vector paddedFrame(frameSize, 0); + const auto& frame = frames[i]; + uint32_t srcRowSize = width * 3; + + for (int y = 0; y < height; ++y) { + int srcY = height - 1 - y; // Flip vertically for BMP format + const uint8_t* srcRow = frame.data() + (srcY * srcRowSize); + uint8_t* dstRow = paddedFrame.data() + (y * rowSize); + memcpy(dstRow, srcRow, srcRowSize); + // Padding bytes remain zeros + } + + std::cout << "11-" << i << std::endl; + // Write frame as '00db' chunk + writeChunk(file, 0x62643030, paddedFrame.data(), frameSize); // '00db' + + // Add to index + AVIIndexEntry entry; + entry.chunkId = 0x62643030; // '00db' + entry.flags = 0x00000010; // AVIIF_KEYFRAME + entry.offset = frameStart; + entry.size = frameSize; + indexEntries.push_back(entry); + } + + std::cout << "12" << std::endl; + // Update movi list size + uint32_t moviListEnd = static_cast(file.tellp()); + file.seekp(moviListStart + 4); + uint32_t moviListSize = moviListEnd - moviListStart - 8; + file.write(reinterpret_cast(&moviListSize), 4); + file.seekp(moviListEnd); + + std::cout << "13" << std::endl; + // idx1 chunk - index + uint32_t idx1Size = static_cast(indexEntries.size() * sizeof(AVIIndexEntry)); + writeChunk(file, 0x31786469, indexEntries.data(), idx1Size); // 'idx1' + + // Update RIFF chunk size + uint32_t fileEnd = static_cast(file.tellp()); + file.seekp(riffStartPos + 4); + uint32_t riffSize = fileEnd - riffStartPos - 8; + file.write(reinterpret_cast(&riffSize), 4); + + std::cout << "14" << std::endl; + return true; + } + + // Convenience function to save from individual frame files + static bool saveAVIFromFrames(const std::string& filename, + const std::vector& frameFiles, + int width, int height, + float fps = 30.0f) { + std::vector> frames; + frames.reserve(frameFiles.size()); + + for (const auto& frameFile : frameFiles) { + std::ifstream file(frameFile, std::ios::binary); + if (!file) { + return false; + } + + // Read BMP file and extract pixel data + file.seekg(0, std::ios::end); + size_t fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector buffer(fileSize); + file.read(reinterpret_cast(buffer.data()), fileSize); + + // Simple BMP parsing - assumes 24-bit uncompressed BMP + if (fileSize < 54 || buffer[0] != 'B' || buffer[1] != 'M') { + return false; + } + + // Extract pixel data offset from BMP header + uint32_t dataOffset = *reinterpret_cast(&buffer[10]); + if (dataOffset >= fileSize) { + return false; + } + + // Read pixel data (BGR format) + std::vector pixelData(buffer.begin() + dataOffset, buffer.end()); + frames.push_back(pixelData); + } + + return saveAVI(filename, frames, width, height, fps); + } +}; + +#endif \ No newline at end of file diff --git a/util/bmpwriter.hpp b/util/output/bmpwriter.hpp similarity index 99% rename from util/bmpwriter.hpp rename to util/output/bmpwriter.hpp index 6c697f3..a3e3a40 100644 --- a/util/bmpwriter.hpp +++ b/util/output/bmpwriter.hpp @@ -7,7 +7,7 @@ #include #include #include -#include "vec3.hpp" +#include "../vectorlogic/vec3.hpp" class BMPWriter { private: diff --git a/util/jxlwriter.hpp b/util/output/jxlwriter.hpp similarity index 100% rename from util/jxlwriter.hpp rename to util/output/jxlwriter.hpp diff --git a/util/vec.cpp b/util/vectorlogic/vec.cpp similarity index 100% rename from util/vec.cpp rename to util/vectorlogic/vec.cpp diff --git a/util/vec2.hpp b/util/vectorlogic/vec2.hpp similarity index 100% rename from util/vec2.hpp rename to util/vectorlogic/vec2.hpp diff --git a/util/vec3.hpp b/util/vectorlogic/vec3.hpp similarity index 100% rename from util/vec3.hpp rename to util/vectorlogic/vec3.hpp diff --git a/util/vec4.hpp b/util/vectorlogic/vec4.hpp similarity index 100% rename from util/vec4.hpp rename to util/vectorlogic/vec4.hpp