Repository: corporateshark/poisson-disk-generator Branch: master Commit: 5339f69a0661 Files: 8 Total size: 34.1 KB Directory structure: gitextract_pvtu5rlt/ ├── .clang-format ├── .github/ │ └── workflows/ │ └── c-cpp.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── Poisson.cpp ├── PoissonGenerator.h └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ --- AccessModifierOffset: -1 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignEscapedNewlines: Left # AlingOperands: true # Unsupported AlignTrailingComments: false AllowAllParametersOfDeclarationOnNextLine: false AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: true AlwaysBreakTemplateDeclarations: true BinPackArguments: false BinPackParameters: false BreakBeforeBinaryOperators: false BreakBeforeBraces: Attach BreakBeforeInheritanceComma: false BreakBeforeTernaryOperators: true BreakConstructorInitializers: AfterColon BreakStringLiterals: true ColumnLimit: 140 CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 2 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DerivePointerAlignment: false FixNamespaceComments: true IncludeBlocks: Preserve IndentCaseLabels: false IndentPPDirectives: None IndentWidth: 2 IndentWrappedFunctionNames: false KeepEmptyLinesAtTheStartOfBlocks: false # LanguageKind: Cpp # Unsupported MaxEmptyLinesToKeep: 1 NamespaceIndentation: None # ObjCBinPackProtocolList: Never # Unsupported ObjCBlockIndentWidth: 2 ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true PenaltyBreakAssignment: 5 PenaltyBreakBeforeFirstCallParameter: 10 PenaltyBreakComment: 60 PenaltyBreakFirstLessLess: 20 PenaltyBreakString: 1000 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 200 PointerAlignment: Left ReflowComments: true SortIncludes: true SortUsingDeclarations: true SpaceAfterCStyleCast: false SpaceAfterTemplateKeyword: false SpaceBeforeAssignmentOperators: true # SpaceBeforeCtorInitializerColon: false # Unsupported # SpaceBeforeInheritanceColon: false # Unsupported SpaceBeforeParens: ControlStatements # SpaceBeforeRangeBasedForLoopColon: true # Unsupported SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInContainerLiterals: true SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Cpp11 TabWidth: 2 UseTab: Never ... ================================================ FILE: .github/workflows/c-cpp.yml ================================================ name: C/C++ CI on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: cmake-build: strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] generator: ["Default Generator", "Unix Makefiles"] env: CMAKE_GENERATOR: >- ${{format(matrix.generator != 'Default Generator' && '-G "{0}"' || '', matrix.generator)}} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 with: submodules: recursive - name: Get number of CPU cores uses: SimenB/github-actions-cpu-cores@v2 - name: Build shell: bash run: | cmake ${{ env.CMAKE_GENERATOR }} -S "${{ github.workspace }}" -B build cd build cmake --build . --parallel ${{ steps.cpu-cores.outputs.count }} ================================================ FILE: .gitignore ================================================ # Compiled Object files *.slo *.lo *.o *.obj # Compiled Dynamic libraries *.so *.dylib *.dll # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # VisualStudio files *.suo *.user *.sdf *.opensdf Debug/ Release/ ipch/ build/ ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.22) project("poisson-disk-generator") add_library(PoissonGenerator INTERFACE PoissonGenerator.h) add_executable(Poisson Poisson.cpp) target_link_libraries(Poisson PRIVATE PoissonGenerator) if(MSVC) target_compile_definitions(Poisson PRIVATE _CRT_SECURE_NO_WARNINGS) endif() set_property(TARGET PoissonGenerator PROPERTY CXX_STANDARD 17) set_property(TARGET PoissonGenerator PROPERTY CXX_STANDARD_REQUIRED ON) ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014-2026 Sergey Kosarevsky Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Poisson.cpp ================================================ /** * \file Poisson.cpp * \brief * * Poisson Disk Points Generator example * * \version 1.7.0 * \date 21/01/2026 * \author Sergey Kosarevsky, 2014-2026 * \author support@linderdaum.com http://www.linderdaum.com http://blog.linderdaum.com */ /* To compile: gcc Poisson.cpp -std=c++17 -lstdc++ */ #include #include #include #include #include #include #include #include #define POISSON_PROGRESS_INDICATOR 1 #include "PoissonGenerator.h" ///////////////// User selectable parameters /////////////////////////////// const int kNumPointsDefaultPoisson = 20000; // default number of points to generate for Poisson disk const int kNumPointsDefaultVogel = 2000; // default number of points to generate for Vogel disk const int kNumPointsDefaultJittered = 2500; // default number of points to generate for jittered grid const int kImageSize = 512; // generate RGB image [ImageSize x ImageSize] //////////////////////////////////////////////////////////////////////////// float* g_DensityMap = nullptr; #if defined(__GNUC__) #define GCC_PACK(n) __attribute__((packed, aligned(n))) #else #define GCC_PACK(n) __declspec(align(n)) #endif // __GNUC__ #pragma pack(push, 1) struct GCC_PACK(1) sBMPHeader { // BITMAPFILEHEADER unsigned short bfType; uint32_t bfSize; unsigned short bfReserved1; unsigned short bfReserved2; uint32_t bfOffBits; // BITMAPINFOHEADER uint32_t biSize; uint32_t biWidth; uint32_t biHeight; unsigned short biPlanes; unsigned short biBitCount; uint32_t biCompression; uint32_t biSizeImage; uint32_t biXPelsPerMeter; uint32_t biYPelsPerMeter; uint32_t biClrUsed; uint32_t biClrImportant; }; #pragma pack(pop) ///////////////// Uncompressed AVI Writer ////////////////////////////////// // Simple uncompressed AVI writer (grayscale 8-bit, no audio) // AVI format reference: https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference class AVIWriter { public: AVIWriter(const char* fileName, int width, int height, int skipFrames) { m_width = width; m_height = height; m_skipFrames = skipFrames; m_frameDataSize = width * height; // grayscale: 1 byte per pixel m_rowPadding = (4 - (width) % 4) % 4; // pad each row to 4-byte boundary (BMP/AVI requirement) m_paddedRowSize = width + m_rowPadding; m_paddedFrameSize = m_paddedRowSize * height; std::cout << "\nSaving video to `" << fileName << "`" << std::endl; m_file.open(fileName, std::ios::out | std::ios::binary); // write placeholder header (will be updated on close) writeHeader(); m_moviStart = static_cast(m_file.tellp()); // start 'movi' LIST writeChunkHeader("LIST", 0); // size placeholder writeFourCC("movi"); } ~AVIWriter() { const uint32_t moviEnd = static_cast(m_file.tellp()); const uint32_t moviSize = moviEnd - m_moviStart - 8; // exclude LIST header writeIndex(); // write index chunk 'idx1' const uint32_t fileEnd = static_cast(m_file.tellp()); // update movi LIST size m_file.seekp(m_moviStart + 4); writeUInt32(moviSize + 4); // +4 for 'movi' fourcc // update RIFF size m_file.seekp(4); writeUInt32(fileEnd - 8); // update frame count in header m_file.seekp(48); // dwTotalFrames in main AVI header writeUInt32(m_frameCount); m_file.seekp(140); // dwLength in stream header writeUInt32(m_frameCount); m_file.close(); std::cout << "\nSaved AVI with " << m_frameCount << " frames" << std::endl; } bool addFrame(const void* bgrData, bool isLastFrame) { if (!isLastFrame && (m_inputFrameCount++ % m_skipFrames) != 0) return false; writeChunkHeader("00db", m_paddedFrameSize); // write frame chunk: '00db' for uncompressed DIB // convert BGR to grayscale and write with row padding const unsigned char* src = static_cast(bgrData); std::vector rowBuffer(m_paddedRowSize, 0); for (int y = 0; y < m_height; y++) { for (int x = 0; x < m_width; x++) { rowBuffer[x] = src[(y * m_width + x) * 3]; // in our grayscale case, just take the first channel } m_file.write(reinterpret_cast(rowBuffer.data()), m_paddedRowSize); } m_frameCount++; return true; } private: std::ofstream m_file; int m_width = 0; int m_height = 0; int m_fps = 60; int m_skipFrames = 16; uint32_t m_inputFrameCount = 0; uint32_t m_frameCount = 0; uint32_t m_frameDataSize = 0; uint32_t m_moviStart = 0; int m_rowPadding = 0; int m_paddedRowSize = 0; uint32_t m_paddedFrameSize = 0; void writeFourCC(const char* fourcc) { m_file.write(fourcc, 4); } void writeUInt32(uint32_t value) { m_file.write(reinterpret_cast(&value), 4); } void writeUInt16(uint16_t value) { m_file.write(reinterpret_cast(&value), 2); } void writeChunkHeader(const char* fourcc, uint32_t size) { writeFourCC(fourcc); writeUInt32(size); } void writeHeader() { writeFourCC("RIFF"); writeUInt32(0); // file size placeholder writeFourCC("AVI "); writeFourCC("LIST"); // hdrl LIST uint32_t hdrlSizePos = static_cast(m_file.tellp()); writeUInt32(0); // size placeholder writeFourCC("hdrl"); // main AVI header (avih) writeFourCC("avih"); writeUInt32(56); // header size writeUInt32(1000000 / m_fps); // dwMicroSecPerFrame writeUInt32(m_paddedFrameSize * m_fps); // dwMaxBytesPerSec writeUInt32(0); // dwPaddingGranularity writeUInt32(0x10); // dwFlags (AVIF_HASINDEX) writeUInt32(0); // dwTotalFrames (placeholder) writeUInt32(0); // dwInitialFrames writeUInt32(1); // dwStreams writeUInt32(m_paddedFrameSize); // dwSuggestedBufferSize writeUInt32(m_width); // dwWidth writeUInt32(m_height); // dwHeight writeUInt32(0); // dwReserved[4] writeUInt32(0); writeUInt32(0); writeUInt32(0); // stream LIST writeFourCC("LIST"); uint32_t strlSizePos = static_cast(m_file.tellp()); writeUInt32(0); // size placeholder writeFourCC("strl"); // stream header (strh) writeFourCC("strh"); writeUInt32(56); // header size writeFourCC("vids"); // fccType writeFourCC("DIB "); // fccHandler (uncompressed) writeUInt32(0); // dwFlags writeUInt16(0); // wPriority writeUInt16(0); // wLanguage writeUInt32(0); // dwInitialFrames writeUInt32(1); // dwScale writeUInt32(m_fps); // dwRate writeUInt32(0); // dwStart writeUInt32(0); // dwLength (placeholder) writeUInt32(m_paddedFrameSize); // dwSuggestedBufferSize writeUInt32(0xFFFFFFFF); // dwQuality writeUInt32(0); // dwSampleSize writeUInt16(0); // rcFrame left writeUInt16(0); // rcFrame top writeUInt16(static_cast(m_width)); // rcFrame right writeUInt16(static_cast(m_height)); // rcFrame bottom // stream format (strf) - BITMAPINFOHEADER + palette for 8-bit grayscale writeFourCC("strf"); writeUInt32(40 + 256 * 4); // header size + palette size writeUInt32(40); // biSize writeUInt32(m_width); // biWidth writeUInt32(m_height); // biHeight writeUInt16(1); // biPlanes writeUInt16(8); // biBitCount (8-bit grayscale) writeUInt32(0); // biCompression (BI_RGB) writeUInt32(m_paddedFrameSize); // biSizeImage writeUInt32(0); // biXPelsPerMeter writeUInt32(0); // biYPelsPerMeter writeUInt32(256); // biClrUsed writeUInt32(256); // biClrImportant // write grayscale palette (256 entries, BGRA format) for (int i = 0; i < 256; i++) { const uint8_t gray = static_cast(i); m_file.put(gray); // B m_file.put(gray); // G m_file.put(gray); // R m_file.put(0); // A (reserved) } // update strl LIST size const uint32_t strlEnd = static_cast(m_file.tellp()); m_file.seekp(strlSizePos); writeUInt32(strlEnd - strlSizePos - 4); m_file.seekp(strlEnd); // Update hdrl LIST size const uint32_t hdrlEnd = static_cast(m_file.tellp()); m_file.seekp(hdrlSizePos); writeUInt32(hdrlEnd - hdrlSizePos - 4); m_file.seekp(hdrlEnd); } void writeIndex() { writeFourCC("idx1"); writeUInt32(m_frameCount * 16); // index size uint32_t offset = 4; // offset from 'movi' to first frame data for (uint32_t i = 0; i < m_frameCount; i++) { writeFourCC("00db"); // chunk ID writeUInt32(0x10); // flags (AVIIF_KEYFRAME) writeUInt32(offset); // offset writeUInt32(m_paddedFrameSize); // size offset += m_paddedFrameSize + 8; // +8 for chunk header } } }; ///////////////// BMP Functions //////////////////////////////////////////// void SaveBMP(const char* FileName, const void* RawBGRImage, int Width, int Height) { sBMPHeader Header; int ImageSize = Width * Height * 3; Header.bfType = 0x4D * 256 + 0x42; Header.bfSize = ImageSize + sizeof(sBMPHeader); Header.bfReserved1 = 0; Header.bfReserved2 = 0; Header.bfOffBits = 0x36; Header.biSize = 40; Header.biWidth = Width; Header.biHeight = Height; Header.biPlanes = 1; Header.biBitCount = 24; Header.biCompression = 0; Header.biSizeImage = ImageSize; Header.biXPelsPerMeter = 6000; Header.biYPelsPerMeter = 6000; Header.biClrUsed = 0; Header.biClrImportant = 0; std::ofstream File(FileName, std::ios::out | std::ios::binary); File.write((const char*)&Header, sizeof(Header)); File.write((const char*)RawBGRImage, ImageSize); std::cout << "Saved " << FileName << std::endl; } unsigned char* LoadBMP(const char* FileName, int* OutWidth, int* OutHeight) { sBMPHeader Header; std::ifstream File(FileName, std::ifstream::binary); File.read((char*)&Header, sizeof(Header)); *OutWidth = Header.biWidth; *OutHeight = Header.biHeight; const size_t DataSize = 3 * Header.biWidth * Header.biHeight; unsigned char* Img = new unsigned char[DataSize]; File.read((char*)Img, DataSize); return Img; } void LoadDensityMap(const char* FileName) { std::cout << "Loading density map " << FileName << std::endl; int W, H; unsigned char* Data = LoadBMP(FileName, &W, &H); std::cout << "Loaded ( " << W << " x " << H << " ) " << std::endl; if (W != kImageSize || H != kImageSize) { std::cout << "ERROR: density map should be " << kImageSize << " x " << kImageSize << std::endl; exit(255); } g_DensityMap = new float[W * H]; for (int y = 0; y != H; y++) { for (int x = 0; x != W; x++) { g_DensityMap[x + y * W] = float(Data[3 * (x + y * W)]) / 255.0f; } } delete[] (Data); } void PrintBanner() { std::cout << "Poisson disk points generator" << std::endl; std::cout << "Version " << PoissonGenerator::Version << std::endl; std::cout << "Sergey Kosarevsky, 2014-2026" << std::endl; std::cout << "support@linderdaum.com http://www.linderdaum.com http://blog.linderdaum.com" << std::endl; std::cout << std::endl; std::cout << "Usage: Poisson [density-map-rgb24.bmp] [--raw-points] [--num-points=] [--square] [--vogel-disk | --jittered-grid | " "--hammersley] [--shuffle] [--save-frames] [--save-video[=]]" << std::endl; std::cout << std::endl; } int main(int argc, char** argv) { PrintBanner(); if (argc > 1 && !strstr(argv[1], "--")) { LoadDensityMap(argv[1]); } auto hasCmdLineArg = [argc, argv](const char* arg) -> bool { for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], arg)) { return true; } } return false; }; auto getCmdLineValue = [argc, argv](const char* arg, unsigned int defaultValue) -> unsigned int { for (int i = 1; i < argc; i++) { if (strstr(argv[i], arg)) { unsigned int v = defaultValue; return sscanf(argv[i], "--num-points=%u", &v) == 1 ? v : defaultValue; } } return defaultValue; }; auto getCmdLineValueSkipFrames = [argc, argv](const char* arg, unsigned int defaultValue) -> unsigned int { for (int i = 1; i < argc; i++) { if (strstr(argv[i], arg)) { unsigned int v = defaultValue; if (sscanf(argv[i], "--save-video=%u", &v) == 1) { return v; } return defaultValue; } } return defaultValue; }; auto hasCmdLineArgPrefix = [argc, argv](const char* prefix) -> bool { for (int i = 1; i < argc; i++) { if (strstr(argv[i], prefix) == argv[i]) { return true; } } return false; }; const bool cmdRawPointsOutput = hasCmdLineArg("--raw-points"); const bool cmdSquare = hasCmdLineArg("--square"); const bool cmdVogelDisk = hasCmdLineArg("--vogel-disk"); const bool cmdJitteredGrid = hasCmdLineArg("--jittered-grid"); const bool cmdHammersley = hasCmdLineArg("--hammersley"); const bool cmdShuffle = hasCmdLineArg("--shuffle"); const bool cmdSaveFrames = hasCmdLineArg("--save-frames"); const bool cmdSaveVideo = hasCmdLineArgPrefix("--save-video"); const unsigned int videoSkipFrames = getCmdLineValueSkipFrames("--save-video", 16); const unsigned int numPoints = getCmdLineValue( "--num-points=", cmdVogelDisk ? kNumPointsDefaultVogel : (cmdJitteredGrid ? kNumPointsDefaultJittered : kNumPointsDefaultPoisson)); std::cout << "NumPoints = " << numPoints << std::endl; PoissonGenerator::DefaultPRNG PRNG; auto Points = cmdVogelDisk ? PoissonGenerator::generateVogelPoints(numPoints) : cmdJitteredGrid ? PoissonGenerator::generateJitteredGridPoints(numPoints, PRNG, !cmdSquare) : cmdHammersley ? PoissonGenerator::generateHammersleyPoints(numPoints) : PoissonGenerator::generatePoissonPoints(numPoints, PRNG, !cmdSquare); // prepare BGR image const size_t DataSize = 3 * kImageSize * kImageSize; unsigned char* Img = new unsigned char[DataSize]; memset(Img, 0, DataSize); if (cmdShuffle) { std::cout << "Shuffling points..." << std::endl; PoissonGenerator::shuffle(Points, PRNG); } std::unique_ptr aviWriter = cmdSaveVideo ? std::make_unique("Points.avi", kImageSize, kImageSize, videoSkipFrames) : nullptr; int frame = 0; size_t currentPoint = 0; const size_t totalPoints = Points.size(); for (const auto& i : Points) { currentPoint++; const int x = int(i.x * kImageSize); const int y = int(i.y * kImageSize); if (x < 0 || y < 0 || x >= kImageSize || y >= kImageSize) continue; if (g_DensityMap) { // dice float R = PRNG.randomFloat(); float P = g_DensityMap[x + y * kImageSize]; if (R > P) continue; } const int Base = 3 * (x + y * kImageSize); Img[Base + 0] = Img[Base + 1] = Img[Base + 2] = 255; if (cmdSaveFrames) { char fileName[64] = {}; snprintf(fileName, sizeof(fileName), "pnt%05i.bmp", frame++); SaveBMP(fileName, Img, kImageSize, kImageSize); } if (aviWriter && aviWriter->addFrame(Img, currentPoint == totalPoints)) { std::cout << "\rRendering points to video: " << currentPoint << "/" << totalPoints << std::flush; } } // always flush the final frame to video if (aviWriter && aviWriter->addFrame(Img, true)) { std::cout << "\rRendering points to video: " << currentPoint << "/" << totalPoints << std::flush; } aviWriter = nullptr; SaveBMP("Points.bmp", Img, kImageSize, kImageSize); delete[] (Img); // dump points to a text file std::ofstream File("points.txt", std::ios::out); if (cmdRawPointsOutput) { File << "NumPoints = " << Points.size() << std::endl; for (const auto& p : Points) { File << p.x << " " << p.y << std::endl; } } else { File << "const vec2 points[" << Points.size() << "]" << std::endl; File << "{" << std::endl; File << std::fixed << std::setprecision(6); for (const auto& p : Points) { File << "\tvec2(" << p.x << "f, " << p.y << "f)," << std::endl; } File << "};" << std::endl; } return 0; } ================================================ FILE: PoissonGenerator.h ================================================ /** * \file PoissonGenerator.h * \brief * * Poisson Disk Points Generator * * \version 1.7.0 * \date 21/01/2026 * \author Sergey Kosarevsky, 2014-2026 * \author support@linderdaum.com http://www.linderdaum.com http://blog.linderdaum.com * * https://github.com/corporateshark/poisson-disk-generator */ /* Usage example: #define POISSON_PROGRESS_INDICATOR 1 #include "PoissonGenerator.h" ... PoissonGenerator::DefaultPRNG PRNG; const auto Points = PoissonGenerator::generatePoissonPoints( NumPoints, PRNG ); ... const auto Points = PoissonGenerator::generateVogelPoints( NumPoints ); */ // Fast Poisson Disk Sampling in Arbitrary Dimensions // http://people.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf // Implementation based on http://devmag.org.za/2009/05/03/poisson-disk-sampling/ /* Versions history: * 1.7 Jan 21, 2026 New arguments: `--shuffle`, `--save-frames`, and `--save-video`; fixed warnings * 1.6.2 Aug 8, 2025 Dropped the `argh` library dependency * 1.6.1 Feb 16, 2024 Reformatted using .clang-format * 1.6 May 29, 2023 Added generateHammersleyPoints() to generate Hammersley points * 1.5 Mar 26, 2022 Added generateJitteredGridPoints() to generate jittered grid points * 1.4.1 Dec 12, 2021 Replaced default Mersenne Twister and with fast and lightweight LCG * 1.4 Dec 5, 2021 Added generateVogelPoints() to generate Vogel disk points * 1.3 Mar 14, 2021 Bugfixes: number of points in the !isCircle mode, incorrect loop boundaries * 1.2 Dec 28, 2019 Bugfixes; more consistent progress indicator; new command line options in demo app * 1.1.6 Dec 7, 2019 Removed duplicate seed initialization; fixed warnings * 1.1.5 Jun 16, 2019 In-class initializers; default ctors; naming, shorter code * 1.1.4 Oct 19, 2016 POISSON_PROGRESS_INDICATOR can be defined outside of the header file, disabled by default * 1.1.3a Jun 9, 2016 Update constructor for DefaultPRNG * 1.1.3 Mar 10, 2016 Header-only library, no global mutable state * 1.1.2 Apr 9, 2015 Output a text file with XY coordinates * 1.1.1 May 23, 2014 Initialize PRNG seed, fixed uninitialized fields * 1.1 May 7, 2014 Support of density maps * 1.0 May 6, 2014 */ #include #include namespace PoissonGenerator { const char* Version = "1.7.0 (21/01/2026)"; class DefaultPRNG { public: DefaultPRNG() = default; explicit DefaultPRNG(unsigned int seed) : seed_(seed) {} inline float randomFloat() { seed_ *= 521167; uint32_t a = (seed_ & 0x007fffff) | 0x40000000; // remap to 0..1 return 0.5f * (*((float*)&a) - 2.0f); } inline uint32_t randomInt(uint32_t maxInt) { return uint32_t(randomFloat() * maxInt); } inline uint32_t getSeed() const { return seed_; } private: uint32_t seed_ = 7133167; }; struct Point { Point() = default; Point(float X, float Y) : x(X), y(Y), valid_(true) {} float x = 0.0f; float y = 0.0f; bool valid_ = false; // bool isInRectangle() const { return x >= 0 && y >= 0 && x <= 1 && y <= 1; } // bool isInCircle() const { const float fx = x - 0.5f; const float fy = y - 0.5f; return (fx * fx + fy * fy) <= 0.25f; } Point& operator+(const Point& p) { x += p.x; y += p.y; return *this; } Point& operator-(const Point& p) { x -= p.x; y -= p.y; return *this; } }; struct GridPoint { GridPoint() = delete; GridPoint(int X, int Y) : x(X), y(Y) {} int x; int y; }; float getDistance(const Point& P1, const Point& P2) { return sqrt((P1.x - P2.x) * (P1.x - P2.x) + (P1.y - P2.y) * (P1.y - P2.y)); } GridPoint imageToGrid(const Point& P, float cellSize) { return GridPoint((int)(P.x / cellSize), (int)(P.y / cellSize)); } struct Grid { Grid(int w, int h, float cellSize) : w_(w), h_(h), cellSize_(cellSize) { grid_.resize(h_); for (auto i = grid_.begin(); i != grid_.end(); i++) { i->resize(w); } } void insert(const Point& p) { const GridPoint g = imageToGrid(p, cellSize_); grid_[g.x][g.y] = p; } bool isInNeighbourhood(const Point& point, float minDist, float cellSize) { const GridPoint g = imageToGrid(point, cellSize); // number of adjacent cells to look for neighbour points const int D = 5; // scan the neighbourhood of the point in the grid for (int i = g.x - D; i <= g.x + D; i++) { for (int j = g.y - D; j <= g.y + D; j++) { if (i >= 0 && i < w_ && j >= 0 && j < h_) { const Point P = grid_[i][j]; if (P.valid_ && getDistance(P, point) < minDist) return true; } } } return false; } private: int w_; int h_; float cellSize_; std::vector> grid_; }; template Point popRandom(std::vector& points, PRNG& generator) { const int idx = generator.randomInt(static_cast(points.size()) - 1); const Point p = points[idx]; points.erase(points.begin() + idx); return p; } template Point generateRandomPointAround(const Point& p, float minDist, PRNG& generator) { // start with non-uniform distribution const float R1 = generator.randomFloat(); const float R2 = generator.randomFloat(); // radius should be between MinDist and 2 * MinDist const float radius = minDist * (R1 + 1.0f); // random angle const float angle = 2 * 3.141592653589f * R2; // the new point is generated around the point (x, y) const float x = p.x + radius * cos(angle); const float y = p.y + radius * sin(angle); return Point(x, y); } /** Return a vector of generated points NewPointsCount - refer to bridson-siggraph07-poissondisk.pdf for details (the value 'k') Circle - 'true' to fill a circle, 'false' to fill a rectangle MinDist - minimal distance estimator, use negative value for default **/ template std::vector generatePoissonPoints(uint32_t numPoints, PRNG& generator, bool isCircle = true, uint32_t newPointsCount = 30, float minDist = -1.0f) { numPoints *= 2; // if we want to generate a Poisson square shape, multiply the estimate number of points by PI/4 due to reduced shape area if (!isCircle) { const double Pi_4 = 0.785398163397448309616; // PI/4 numPoints = static_cast(Pi_4 * numPoints); } if (minDist < 0.0f) { minDist = sqrt(float(numPoints)) / float(numPoints); } std::vector samplePoints; std::vector processList; if (!numPoints) return samplePoints; // create the grid const float cellSize = minDist / sqrt(2.0f); const int gridW = (int)ceil(1.0f / cellSize); const int gridH = (int)ceil(1.0f / cellSize); Grid grid(gridW, gridH, cellSize); Point firstPoint; do { firstPoint = Point(generator.randomFloat(), generator.randomFloat()); } while (!(isCircle ? firstPoint.isInCircle() : firstPoint.isInRectangle())); // update containers processList.push_back(firstPoint); samplePoints.push_back(firstPoint); grid.insert(firstPoint); #if POISSON_PROGRESS_INDICATOR size_t progress = 0; #endif // generate new points for each point in the queue while (!processList.empty() && samplePoints.size() <= numPoints) { #if POISSON_PROGRESS_INDICATOR // a progress indicator, kind of if ((samplePoints.size()) % 1000 == 0) { const size_t newProgress = 200 * (samplePoints.size() + processList.size()) / numPoints; if (newProgress != progress) { progress = newProgress; std::cout << "."; } } #endif // POISSON_PROGRESS_INDICATOR const Point point = popRandom(processList, generator); for (uint32_t i = 0; i < newPointsCount; i++) { const Point newPoint = generateRandomPointAround(point, minDist, generator); const bool canFitPoint = isCircle ? newPoint.isInCircle() : newPoint.isInRectangle(); if (canFitPoint && !grid.isInNeighbourhood(newPoint, minDist, cellSize)) { processList.push_back(newPoint); samplePoints.push_back(newPoint); grid.insert(newPoint); continue; } } } #if POISSON_PROGRESS_INDICATOR std::cout << std::endl << std::endl; #endif // POISSON_PROGRESS_INDICATOR return samplePoints; } Point sampleVogelDisk(uint32_t idx, uint32_t numPoints, float phi) { const float kGoldenAngle = 2.4f; const float r = sqrtf(float(idx) + 0.5f) / sqrtf(float(numPoints)); const float theta = idx * kGoldenAngle + phi; return Point(r * cosf(theta), r * sinf(theta)); } /** Return a vector of generated points **/ std::vector generateVogelPoints(uint32_t numPoints, bool isCircle = true, float phi = 0.0f, Point center = Point(0.5f, 0.5f)) { std::vector samplePoints; samplePoints.reserve(numPoints); const uint32_t numSamples = isCircle ? 4 * numPoints : numPoints; for (uint32_t i = 0; i != numPoints; i++) { const Point p = sampleVogelDisk(i, numSamples, phi * 3.141592653f / 180.0f) + center; samplePoints.push_back(p); } return samplePoints; } /** Return a vector of generated points **/ template std::vector generateJitteredGridPoints(uint32_t numPoints, PRNG& generator, bool isCircle = false, float jitterRadius = 0.004f, Point center = Point(0.5f, 0.5f)) { std::vector samplePoints; samplePoints.reserve(numPoints); const uint32_t gridSize = uint32_t(sqrt(numPoints)); for (uint32_t x = 0; x != gridSize; x++) { for (uint32_t y = 0; y != gridSize; y++) { Point p; do { const Point offs = generateRandomPointAround(Point(0, 0), jitterRadius, generator) - center + Point(0.5f, 0.5f); p = Point(float(x) / gridSize, float(y) / gridSize) + offs; // generate a new point until it is within the boundaries } while (!p.isInRectangle()); if (isCircle) if (!p.isInCircle()) continue; samplePoints.push_back(p); } } return samplePoints; } namespace { // http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html float radicalInverse_VdC(uint32_t bits) { bits = (bits << 16u) | (bits >> 16u); bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); return float(float(bits) * 2.3283064365386963e-10); // / 0x100000000 } Point hammersley2d(uint32_t i, uint32_t N) { return Point(float(i) / float(N), radicalInverse_VdC(i)); } } // namespace /** Return a vector of generated points **/ std::vector generateHammersleyPoints(uint32_t numPoints) { std::vector samplePoints; samplePoints.reserve(numPoints); const uint32_t gridSize = uint32_t(sqrt(numPoints)); for (uint32_t i = 0; i != numPoints; i++) { Point p = hammersley2d(i, numPoints); samplePoints.push_back(p); } return samplePoints; } template void shuffle(std::vector& points, PRNG& generator) { const int length = (int)points.size(); if (!length) return; // Fisher-Yates shuffle for (int i = length - 1; i-- > 0;) { std::swap(points[i], points[generator.randomInt(i)]); } } } // namespace PoissonGenerator ================================================ FILE: README.md ================================================ **Poisson Disk Points Generator** (C) Sergey Kosarevsky, 2014-2026 @corporateshark sk@linderdaum.com http://www.linderdaum.com http://blog.linderdaum.com ============================= Poisson disk & Vogel disk points generator in a single file header-only C++11 library. Usage example: -------------- ``` #define POISSON_PROGRESS_INDICATOR 1 #include "PoissonGenerator.h" ... PoissonGenerator::DefaultPRNG PRNG; const auto Points = PoissonGenerator::generatePoissonPoints( numPoints, PRNG ); ... const auto Points = PoissonGenerator::generateVogelPoints( numPoints ); ... const auto Points = PoissonGenerator::generateJitteredGridPoints( numPoints, PRNG ); ... const auto Points = PoissonGenerator::generateHammersleyPoints( numPoints ); ``` Build instructions: ----------- Linux/OSX: ```gcc Poisson.cpp -std=c++17 -lstdc++``` Windows: ```cmake -G "Visual Studio 17 2022" -A x64``` Demo app usage: --------------- Poisson [density-map-rgb24.bmp] [--raw-points] [--num-points=] [--shuffle] [--save-frames] [--save-video[=]] [--save-frames] [--square] [--vogel-disk | --jittered-grid] Algorithm description can be found in "Fast Poisson Disk Sampling in Arbitrary Dimensions" http://people.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf Implementation is based on http://devmag.org.za/2009/05/03/poisson-disk-sampling/ ============================= Poisson disk ![Poisson disk](.github/1_Poisson_disk.png) Poisson rectangle ![Poisson rectange](.github/2_Poisson_rect.png) Poisson rectangle with custom density map ![Poisson rectangle with custom density map](.github/3_Poisson_density.png) Vogel disk ![Vogel disk](.github/4_Vogel_disk.png) Jittered grid ![image](.github/5_Jittered_grid.png) Hammersley points ![Points](.github/6_Hammersley.png)