From 7cdac8ded95ea4ff0ef488e975b30b262ade5612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Eng=C3=A9libert?= Date: Sat, 26 Jul 2025 22:14:37 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + APSF.cpp | 272 +++++++++++++ APSF.h | 155 ++++++++ BlueNoise/BLUE_NOISE.cpp | 448 +++++++++++++++++++++ BlueNoise/BLUE_NOISE.h | 183 +++++++++ BlueNoise/LICENSE.txt | 7 + BlueNoise/README.txt | 101 +++++ BlueNoise/RNG.cpp | 174 +++++++++ BlueNoise/RNG.h | 37 ++ BlueNoise/RangeList.cpp | 139 +++++++ BlueNoise/RangeList.h | 27 ++ BlueNoise/ScallopedSector.cpp | 289 ++++++++++++++ BlueNoise/ScallopedSector.h | 51 +++ BlueNoise/WeightedDiscretePDF.cpp | 313 +++++++++++++++ BlueNoise/WeightedDiscretePDF.h | 57 +++ CELL.cpp | 225 +++++++++++ CELL.h | 194 ++++++++++ CG_SOLVER.cpp | 309 +++++++++++++++ CG_SOLVER.h | 120 ++++++ CG_SOLVER_SSE.cpp | 416 ++++++++++++++++++++ CG_SOLVER_SSE.h | 103 +++++ DAG.cpp | 566 +++++++++++++++++++++++++++ DAG.h | 208 ++++++++++ EXR.cpp | 96 +++++ EXR.h | 92 +++++ FFT.cpp | 233 +++++++++++ FFT.h | 95 +++++ LumosQuad.sln | 20 + LumosQuad.vcproj | 322 ++++++++++++++++ Makefile | 52 +++ QUAD_DBM_2D.cpp | 461 ++++++++++++++++++++++ QUAD_DBM_2D.h | 196 ++++++++++ QUAD_POISSON.cpp | 620 ++++++++++++++++++++++++++++++ QUAD_POISSON.h | 190 +++++++++ README.md | 10 + examples/hint.ppm | Bin 0 -> 196668 bytes examples/nopath.ppm | Bin 0 -> 196668 bytes examples/spine.lightning | Bin 0 -> 193342 bytes examples/spine.ppm | Bin 0 -> 196623 bytes examples/spineless.ppm | Bin 0 -> 196668 bytes examples/y-second.ppm | Bin 0 -> 196668 bytes examples/y.ppm | Bin 0 -> 196668 bytes main.cpp | 408 ++++++++++++++++++++ ppm/ppm.cpp | 60 +++ ppm/ppm.hpp | 23 ++ 45 files changed, 7275 insertions(+) create mode 100644 .gitignore create mode 100644 APSF.cpp create mode 100644 APSF.h create mode 100644 BlueNoise/BLUE_NOISE.cpp create mode 100644 BlueNoise/BLUE_NOISE.h create mode 100644 BlueNoise/LICENSE.txt create mode 100644 BlueNoise/README.txt create mode 100644 BlueNoise/RNG.cpp create mode 100644 BlueNoise/RNG.h create mode 100644 BlueNoise/RangeList.cpp create mode 100644 BlueNoise/RangeList.h create mode 100644 BlueNoise/ScallopedSector.cpp create mode 100644 BlueNoise/ScallopedSector.h create mode 100644 BlueNoise/WeightedDiscretePDF.cpp create mode 100644 BlueNoise/WeightedDiscretePDF.h create mode 100644 CELL.cpp create mode 100644 CELL.h create mode 100644 CG_SOLVER.cpp create mode 100644 CG_SOLVER.h create mode 100644 CG_SOLVER_SSE.cpp create mode 100644 CG_SOLVER_SSE.h create mode 100644 DAG.cpp create mode 100644 DAG.h create mode 100644 EXR.cpp create mode 100644 EXR.h create mode 100644 FFT.cpp create mode 100644 FFT.h create mode 100644 LumosQuad.sln create mode 100644 LumosQuad.vcproj create mode 100644 Makefile create mode 100644 QUAD_DBM_2D.cpp create mode 100644 QUAD_DBM_2D.h create mode 100644 QUAD_POISSON.cpp create mode 100644 QUAD_POISSON.h create mode 100644 README.md create mode 100644 examples/hint.ppm create mode 100644 examples/nopath.ppm create mode 100644 examples/spine.lightning create mode 100644 examples/spine.ppm create mode 100644 examples/spineless.ppm create mode 100644 examples/y-second.ppm create mode 100644 examples/y.ppm create mode 100644 main.cpp create mode 100644 ppm/ppm.cpp create mode 100644 ppm/ppm.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6aa15dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/* +html +*.exr diff --git a/APSF.cpp b/APSF.cpp new file mode 100644 index 0000000..74b2fbf --- /dev/null +++ b/APSF.cpp @@ -0,0 +1,272 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : APSF.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#include "APSF.h" +#include + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +APSF::APSF(int res) : + _res(res) +{ + // make sure kernel is odd dimensions + if (!(_res % 2)) _res++; + _kernel = new float[_res * _res]; + + _q = 0.999; + _R = 400.0f; + _D = 2000.0f; + _T = 1.001f; + _sigma = 0.5f; + + _maxTerms = 600; + _I0 = 1.0f; + _retinaSize = 0.01f; + _eyeSize = 0.025f; +} + +APSF::~APSF() +{ + delete[] _kernel; +} + +////////////////////////////////////////////////////////////////////// +// Legendre polymonial +////////////////////////////////////////////////////////////////////// +float APSF::legendreM(int m, float mu) +{ + vector memoized; + memoized.push_back(1.0f); + memoized.push_back(mu); + + for (int x = 2; x <= m; x++) + { + float newMemo = ((2.0f * (float)x - 1.0f) * mu * memoized[x - 1] - + ((float)x - 1.0f) * memoized[x - 2]) / (float)x; + memoized.push_back(newMemo); + } + + return memoized[m]; +} + +////////////////////////////////////////////////////////////////////// +// scattering function at a point +////////////////////////////////////////////////////////////////////// +float APSF::pointAPSF(float mu) +{ + float total = 0.0f; + + for (int m = 0; m < _maxTerms; m++) + total += (gM(_I0, m) + gM(_I0, m + 1)) * legendreM(m, mu); + + return total; +} + +////////////////////////////////////////////////////////////////////// +// generate a convolution kernel +////////////////////////////////////////////////////////////////////// +void APSF::generateKernelFast() +{ + float dx = _retinaSize / (float)_res; + float dy = _retinaSize / (float)_res; + int halfRes = _res / 2; + float* oneD = new float[_res]; + + float max = 0.0f; + float min = 1000.0f; + int x,y = halfRes; + for (x = 0; x < _res; x++) + { + // calc angle + float diffX = (x - halfRes) * dx; + float diffY = (y - halfRes) * dy; + float distance = sqrt(diffX * diffX + diffY * diffY); + if ((distance / _eyeSize) > (_R / _D)) + oneD[x] = 0.0f; + else + { + float i = -distance * distance * _D * _D + _eyeSize * _eyeSize * _R * _R + distance * distance * _R * _R; + i = _eyeSize * _eyeSize * _D - _eyeSize * sqrt(i); + i /= _eyeSize * _eyeSize + distance * distance; + float mu = M_PI - atan(_retinaSize / distance) - asin((_D - i) / _R); + oneD[x] = pointAPSF(cos(mu)); + + min = (oneD[x] < min) ? oneD[x] : min; + } + max = (oneD[x] > max) ? oneD[x] : max; + } + + // floor + if (min > 0.0f) + { + for (int i = 0; i < _res; i++) + if (oneD[i] > 0.0f) + oneD[i] -= min; + max -= min; + } + + // normalize + if (max > 1.0f) + { + float maxInv = 1.0f / max; + for (int i = 0; i < _res; i++) + oneD[i] *= maxInv; + } + + // interpolate the kernel + int index = 0; + for (y = 0; y < _res; y++) + for (x = 0; x < _res; x++, index++) + { + float dx = fabs((float)(x - halfRes)); + float dy = fabs((float)(y - halfRes)); + float magnitude = sqrtf(dx * dx + dy * dy); + + int lower = floor(magnitude); + if (lower > halfRes - 1) + { + _kernel[index] = 0.0f; + continue; + } + float lerp = magnitude - lower; + _kernel[index] = (1.0f - lerp) * oneD[halfRes + lower] + + lerp * oneD[halfRes + lower + 1]; + + } + + delete[] oneD; +} + +////////////////////////////////////////////////////////////////////// +// save the kernel in binary +////////////////////////////////////////////////////////////////////// +void APSF::write(const char* filename) +{ + // open file + FILE* file; + file = fopen(filename, "wb"); + + fwrite((void*)&_res, sizeof(int), 1, file); + fwrite((void*)&_q, sizeof(float), 1, file); + fwrite((void*)&_T, sizeof(float), 1, file); + fwrite((void*)&_I0, sizeof(float), 1, file); + fwrite((void*)&_sigma, sizeof(float), 1, file); + fwrite((void*)&_R, sizeof(float), 1, file); + fwrite((void*)&_D, sizeof(float), 1, file); + fwrite((void*)&_retinaSize, sizeof(float), 1, file); + fwrite((void*)&_eyeSize, sizeof(float), 1, file); + fwrite((void*)&_maxTerms, sizeof(int), 1, file); + fwrite((void*)&_kernel, sizeof(float) * _res * _res, 1, file); + + fclose(file); +} + +////////////////////////////////////////////////////////////////////// +// load a binary kernel +////////////////////////////////////////////////////////////////////// +void APSF::read(const char* filename) +{ + // open file + FILE* file; + file = fopen(filename, "rb"); + + if (_kernel) delete[] _kernel; + + fread((void*)&_res, sizeof(int), 1, file); + fread((void*)&_q, sizeof(float), 1, file); + fread((void*)&_T, sizeof(float), 1, file); + fread((void*)&_I0, sizeof(float), 1, file); + fread((void*)&_sigma, sizeof(float), 1, file); + fread((void*)&_R, sizeof(float), 1, file); + fread((void*)&_D, sizeof(float), 1, file); + fread((void*)&_retinaSize, sizeof(float), 1, file); + fread((void*)&_eyeSize, sizeof(float), 1, file); + fread((void*)&_maxTerms, sizeof(int), 1, file); + + _kernel = new float[_res * _res]; + fread((void*)&_kernel, sizeof(float) * _res * _res, 1, file); + + fclose(file); +} + +////////////////////////////////////////////////////////////////////// +// write the kernel to a PPM file +////////////////////////////////////////////////////////////////////// +void APSF::writePPM(const char* filename) +{ + unsigned char* ppm = new unsigned char[3 * _res * _res]; + + for (int x = 0; x < _res * _res; x++) + { + ppm[3 * x] = 255 * _kernel[x]; + ppm[3 * x + 1] = 255 * _kernel[x]; + ppm[3 * x + 2] = 255 * _kernel[x]; + } + WritePPM(filename, ppm, _res, _res); + + delete[] ppm; +} diff --git a/APSF.h b/APSF.h new file mode 100644 index 0000000..466dd62 --- /dev/null +++ b/APSF.h @@ -0,0 +1,155 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : APSF.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#ifndef APSF_H +#define APSF_H + +#include +#include +#include +#include "ppm/ppm.hpp" + +using namespace std; + +#ifndef M_PI +#define M_PI 3.1415926535897931f +#endif + +//////////////////////////////////////////////////////////////////// +/// \brief Generates the rendering filter +//////////////////////////////////////////////////////////////////// +class APSF +{ +public: + //! constructor + APSF(int res = 512); + //! destructor + virtual ~APSF(); + + //! read in an APSF file + void read(const char* filename); + //! write out an APSF file + void write(const char* filename); + //! write out the current kernel to a PPM file + void writePPM(const char* filename); + + //! generate one line of the kernel and spin it radially + void generateKernelFast(); + + //! resolution of current kernel + int res() { return _res; }; + + //! returns the float array for the kernel + float* kernel() { return _kernel; }; + +private: + //! kernel resolution + int _res; + //! convolution kernel + float* _kernel; + + //////////////////////////////////////////////////////////////////// + // APSF components + //////////////////////////////////////////////////////////////////// + + // scattering parameters + float _q; + float _T; + float _I0; + float _sigma; + float _R; + float _D; + + float _retinaSize; + float _eyeSize; + + //! number of coefficients + int _maxTerms; + + //! function value at a point + float pointAPSF(float mu); + + //////////////////////////////////////////////////////////////////// + // auxiliary functions + //////////////////////////////////////////////////////////////////// + float legendreM(int m, float mu); + float gM(float I0, int m) { + return (m == 0) ? 0.0f : exp(-(betaM(m, _q) * _T + alphaM(m) * log(_T))); + }; + float alphaM(float m) { + return m + 1.0f; + }; + float betaM(float m, float q) { + return ((2.0f * m + 1.0f) / m) * (1.0f - pow(q, (int)m - 1)); + }; + float factorial(float x) { + return (x <= 1.0f) ? 1.0f : x * factorial(x - 1.0f); + }; + float choose(float x, float y) { + return factorial(x) / (factorial(y) * factorial(x - y)); + }; +}; + +#endif diff --git a/BlueNoise/BLUE_NOISE.cpp b/BlueNoise/BLUE_NOISE.cpp new file mode 100644 index 0000000..3adb10e --- /dev/null +++ b/BlueNoise/BLUE_NOISE.cpp @@ -0,0 +1,448 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : BLUE_NOISE.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This class is a very thin wrapper to Daniel Dunbar's blue noise generator. +// With the exception of BLUE_NOISE.h and BLUE_NOISE.cpp, the other files +// in this directory are unmodified copies of his code. +// +// For the original, untainted code, see: +// http://www.cs.virginia.edu/~gfx/pubs/antimony/ +// +/////////////////////////////////////////////////////////////////////////////////// + +// $Id: PDSampling.cpp,v 1.12 2006/07/11 16:45:22 zr Exp $ + +#define _USE_MATH_DEFINES +#include + +#include + +#include "BLUE_NOISE.h" +#include "RangeList.h" +#include "ScallopedSector.h" +#include "WeightedDiscretePDF.h" + +typedef std::vector IntVector; + +/// + +BLUE_NOISE::BLUE_NOISE(float _radius, bool _isTiled, bool usesGrid) : + m_rng(123456), + radius(_radius), + isTiled(_isTiled) +{ + if (usesGrid) { + // grid size is chosen so that 4*radius search only + // requires searching adjacent cells, this also + // determines max points per cell + m_gridSize = (int) ceil(2./(4.*_radius)); + if (m_gridSize<2) m_gridSize = 2; + + m_gridCellSize = 2.0f/m_gridSize; + m_grid = new int[m_gridSize*m_gridSize][kMaxPointsPerCell]; + + for (int y=0; y=a.x && 1>=a.y; +} + +Vec2 BLUE_NOISE::randomPoint() +{ + return Vec2(2*m_rng.getFloatL()-1, 2*m_rng.getFloatL()-1); +} + +Vec2 BLUE_NOISE::getTiled(Vec2 v) +{ + float x = v.x, y = v.y; + + if (isTiled) { + if (x<-1) x += 2; + else if (x>1) x -= 2; + + if (y<-1) y += 2; + else if (y>1) y -= 2; + } + + return Vec2(x,y); +} + +void BLUE_NOISE::getGridXY(Vec2 &v, int *gx_out, int *gy_out) +{ + int gx = *gx_out = (int) floor(.5*(v.x + 1)*m_gridSize); + int gy = *gy_out = (int) floor(.5*(v.y + 1)*m_gridSize); + if (gx<0 || gx>=m_gridSize || gy<0 || gy>=m_gridSize) { + printf("Internal error, point outside grid was generated, ignoring.\n"); + } +} + +void BLUE_NOISE::addPoint(Vec2 pt) +{ + int i, gx, gy, *cell; + + points.push_back(pt); + + if (m_grid) { + getGridXY(pt, &gx, &gy); + cell = m_grid[gy*m_gridSize + gx]; + for (i=0; i(m_gridSize>>1)) N = m_gridSize>>1; + + m_neighbors.clear(); + getGridXY(pt, &gx, &gy); + for (j=-N; j<=N; j++) { + for (i=-N; i<=N; i++) { + int cx = (gx+i+m_gridSize)%m_gridSize; + int cy = (gy+j+m_gridSize)%m_gridSize; + int *cell = m_grid[cy*m_gridSize + cx]; + + for (k=0; k(m_gridSize>>1)) N = m_gridSize>>1; + + getGridXY(pt, &gx, &gy); + for (j=-N; j<=N; j++) { + for (i=-N; i<=N; i++) { + int cx = (gx+i+m_gridSize)%m_gridSize; + int cy = (gy+j+m_gridSize)%m_gridSize; + int *cell = m_grid[cy*m_gridSize + cx]; + + for (k=0; k(m_gridSize>>1)) N = m_gridSize>>1; + + getGridXY(candidate, &gx, &gy); + + int xSide = (candidate.x - (-1 + gx*m_gridCellSize))>m_gridCellSize*.5; + int ySide = (candidate.y - (-1 + gy*m_gridCellSize))>m_gridCellSize*.5; + int iy = 1; + for (j=-N; j<=N; j++) { + int ix = 1; + + if (j==0) iy = ySide; + else if (j==1) iy = 0; + + for (i=-N; i<=N; i++) { + if (i==0) ix = xSide; + else if (i==1) ix = 0; + + // offset to closest cell point + float dx = candidate.x - (-1 + (gx+i+ix)*m_gridCellSize); + float dy = candidate.y - (-1 + (gy+j+iy)*m_gridCellSize); + + if (dx*dx+dy*dy relaxTmpOut.txt"); + + tmp = fopen("relaxTmpOut.txt", "r"); + fscanf(tmp, "%d\n%d\n", &dim, &numVerts); + + if (dim!=2) { + printf("Error calling out to qvoronoi, skipping relaxation.\n"); + goto exit; + } + + verts = new Vec2[numVerts]; + for (int i=0; i RegionMap; + +void BLUE_NOISE::complete() +{ + RangeList rl(0,0); + IntVector candidates; + + addPoint(randomPoint()); + candidates.push_back((int) points.size()-1); + + while (candidates.size()) { + int c = m_rng.getInt32()%candidates.size(); + int index = candidates[c]; + Vec2 candidate = points[index]; + candidates[c] = candidates[candidates.size()-1]; + candidates.pop_back(); + + rl.reset(0, (float) M_PI*2); + findNeighborRanges(index, rl); + while (rl.numRanges) { + RangeEntry &re = rl.ranges[m_rng.getInt32()%rl.numRanges]; + float angle = re.min + (re.max-re.min)*m_rng.getFloatL(); + Vec2 pt = getTiled(Vec2(candidate.x + cos(angle)*2*radius, + candidate.y + sin(angle)*2*radius)); + + addPoint(pt); + candidates.push_back((int) points.size()-1); + + rl.subtract(angle - (float) M_PI/3, angle + (float) M_PI/3); + } + } +} + +void BLUE_NOISE::writeToBool(bool* noise, int size) +{ + // wipe + int index = 0; + for (index = 0; index < size * size; index++) + noise[index] = false; + + for (int x = 0; x < points.size(); x++) + { + int i = (points[x].x + 1.0f) * 0.5f * size; + int j = (points[x].y + 1.0f) * 0.5f * size; + index = i + j * size; + noise[index] = true; + } +} diff --git a/BlueNoise/BLUE_NOISE.h b/BlueNoise/BLUE_NOISE.h new file mode 100644 index 0000000..dbeab3e --- /dev/null +++ b/BlueNoise/BLUE_NOISE.h @@ -0,0 +1,183 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : BLUE_NOISE.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This class is a very thin wrapper to Daniel Dunbar's blue noise generator. +// With the exception of BLUE_NOISE.h and BLUE_NOISE.cpp, the other files +// in this directory are unmodified copies of his code. +// +// For the original, untainted code, see: +// http://www.cs.virginia.edu/~gfx/pubs/antimony/ +// +/////////////////////////////////////////////////////////////////////////////////// + +// $Id: PDSampling.h,v 1.6 2006/07/06 23:13:18 zr Exp $ + +#include "RNG.h" +#include +#include + +#define kMaxPointsPerCell 9 + +class RangeList; +class ScallopedRegion; + +class Vec2 { +public: + Vec2() {}; + Vec2(float _x, float _y) : x(_x), y(_y) {}; + + float x,y; + + float length() { return sqrt(x*x + y*y); } + + bool operator ==(const Vec2 &b) const { return x==b.x && y==b.y; } + Vec2 operator +(Vec2 b) { return Vec2(x+b.x, y+b.y); } + Vec2 operator -(Vec2 b) { return Vec2(x-b.x, y-b.y); } + Vec2 operator *(Vec2 b) { return Vec2(x*b.x, y*b.y); } + Vec2 operator /(Vec2 b) { return Vec2(x/b.x, y*b.y); } + + Vec2 operator +(float n) { return Vec2(x+n, y+n); } + Vec2 operator -(float n) { return Vec2(x-n, y-n); } + Vec2 operator *(float n) { return Vec2(x*n, y*n); } + Vec2 operator /(float n) { return Vec2(x/n, y*n); } + + Vec2 &operator +=(Vec2 b) { x+=b.x; y+=b.y; return *this; } + Vec2 &operator -=(Vec2 b) { x-=b.x; y-=b.y; return *this; } + Vec2 &operator *=(Vec2 b) { x*=b.x; y*=b.y; return *this; } + Vec2 &operator /=(Vec2 b) { x/=b.x; y/=b.y; return *this; } + + Vec2 &operator +=(float n) { x+=n; y+=n; return *this; } + Vec2 &operator -=(float n) { x-=n; y-=n; return *this; } + Vec2 &operator *=(float n) { x*=n; y*=n; return *this; } + Vec2 &operator /=(float n) { x/=n; y/=n; return *this; } +}; + +/// \brief Daniel Dunbar's blue noise generator +/// +/// The original code has been modified so that the 'boundary sampling' +/// method is the only one available. +class BLUE_NOISE { +protected: + RNG m_rng; + std::vector m_neighbors; + + int (*m_grid)[kMaxPointsPerCell]; + int m_gridSize; + float m_gridCellSize; + +public: + std::vector points; + float radius; + bool isTiled; + +public: + BLUE_NOISE(float radius, bool isTiled=true, bool usesGrid=true); + virtual ~BLUE_NOISE() { }; + + // + + bool pointInDomain(Vec2 &a); + + // return shortest distance between _a_ + // and _b_ (accounting for tiling) + float getDistanceSquared(Vec2 &a, Vec2 &b) { Vec2 v = getTiled(b-a); return v.x*v.x + v.y*v.y; } + float getDistance(Vec2 &a, Vec2 &b) { return sqrt(getDistanceSquared(a, b)); } + + // generate a random point in square + Vec2 randomPoint(); + + // return tiled coordinates of _v_ + Vec2 getTiled(Vec2 v); + + // return grid x,y for point + void getGridXY(Vec2 &v, int *gx_out, int *gy_out); + + // add _pt_ to point list and grid + void addPoint(Vec2 pt); + + // populate m_neighbors with list of + // all points within _radius_ of _pt_ + // and return number of such points + int findNeighbors(Vec2 &pt, float radius); + + // return distance to closest neighbor within _radius_ + float findClosestNeighbor(Vec2 &pt, float radius); + + // find available angle ranges on boundary for candidate + // by subtracting occluded neighbor ranges from _rl_ + void findNeighborRanges(int index, RangeList &rl); + + // extend point set by boundary sampling until domain is + // full + void maximize(); + + // apply one step of Lloyd relaxation + void relax(); + + void complete(); + + void writeToBool(bool* noise, int size); +}; diff --git a/BlueNoise/LICENSE.txt b/BlueNoise/LICENSE.txt new file mode 100644 index 0000000..07697aa --- /dev/null +++ b/BlueNoise/LICENSE.txt @@ -0,0 +1,7 @@ +This code is released into the public domain. You can do whatever +you want with it. + +I do ask that you respect the authorship and credit myself (Daniel +Dunbar) when referencing the code. Additionally, if you use the +code in an interesting or integral manner I would like to hear +about it. diff --git a/BlueNoise/README.txt b/BlueNoise/README.txt new file mode 100644 index 0000000..af89ac4 --- /dev/null +++ b/BlueNoise/README.txt @@ -0,0 +1,101 @@ +PDSample - Poisson-Disk sample set generation +Daniel Dunbar, daniel@zuster.org +---- + + +Overview +-- +PDSample generates Poisson-disk sampling sets in the domain [-1,1]^2 +using a variety of methods. See "A Spatial Data Structure for Fast +Poisson-Disk Sample Generation", in Proc' of SIGGRAPH 2006 for more +information. + + +Building +--- +The code should be portable to any platform with 32-bit float's and int's. + +Windows: There is an included PDSample.sln for MSVS version 7. +Unix: Type 'make' and hope for the best. + + +Usage +--- +PDSample [-m] [-t] [-r ] [-M ] + [-N ] + +Options +-- + o -t + Uses tiled (toroidal) domain for supporting samplers. The resulting point + set will be suitable for tiling repeatedly in the x and y directions. + + o -m + Maximize the resulting point set. For samplers which do not already produce + a maximal point set then this will use the Boundary sampling method to + ensure the resulting point set is maximal. + + o -r + Apply the specified number of relaxations to the resulting point set. This + requires that qvoronoi be in the path. + + o -M + For DartThrowing and BestCandidate methods this determines the factor to + multiply the current number of points by to determine how many samples to + take before exiting (DartThrowing) or accepting the best candidate + (BestCandidate). + + o -N + This specifies a minimum number of samples that will be taken for the + DartThrowing sampler. See below. + + +Available Samplers (for method argument) +-- + o DartThrowing + Standard dart throwing. On each iteration the DartThrowing sampler will + try min(N*multiplier,minMaxThrows) samples before termination. Note that + for regular dart throwing where simply a maximum number of throws is used + to determine the termination point, the multiplier should be set to 0. + + o BestCandidate + Mitchell's Best Candidate algorithm. Uses the multiplier argument. + + o Boundary + Dart throwing by maximizing boundaries. + + o Pure + Dart throwing using scalloped sectors. + o LinearPure + Dart throwing using scalloped sectors but without sampling regions according + to their probability of being hit. + + o Penrose + Ostromoukhov et al.'s sampling method using their quasisampler_prototype.h + + o Uniform + Random point generation. The number of samples to take is calculated as + .75/radius^2 to approximately match the density of Poisson-disk sampling. + + +Output format +-- +Point sets are output in a trivial binary format. The format is not intended +for distribution and does not encode the endianness of the generating platform. + +The format matches the pseudo-C struct below: +struct { + int N; // number of points + float t; // generation time + float r; // radius used in generation + int isTiled; // flag for if the set is tileable + float points[N][2]; +}; + + +Acknowledgments +-- +Thanks to Ares Lagae for comments on a preliminary release of the code, +Ostromoukhov et al. for making available their quasisampler implementation, +as well as Takuji Nishimura and Makoto Matsumoto for their Mersenne +Twister random number generator. diff --git a/BlueNoise/RNG.cpp b/BlueNoise/RNG.cpp new file mode 100644 index 0000000..b7e3f8a --- /dev/null +++ b/BlueNoise/RNG.cpp @@ -0,0 +1,174 @@ +/* + A C-program for MT19937, with initialization improved 2002/1/26. + Coded by Takuji Nishimura and Makoto Matsumoto. + Modified to be a C++ class by Daniel Dunbar. + + Before using, initialize the state by using init_genrand(seed) + or init_by_array(init_key, key_length). + + Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. The names of its contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + Any feedback is very welcome. + http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html + email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) +*/ + +#include "RNG.h" + +/* initializes mt[N] with a seed */ +RNG::RNG(unsigned long s) +{ + seed(s); +} + +/* initialize by an array with array-length */ +/* init_key is the array for initializing keys */ +/* key_length is its length */ +/* slight change for C++, 2004/2/26 */ +RNG::RNG(unsigned long init_key[], int key_length) +{ + int i, j, k; + seed(19650218UL); + i=1; j=0; + k = (N>key_length ? N : key_length); + for (; k; k--) { + mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * 1664525UL)) + + init_key[j] + j; /* non linear */ + mt[i] &= 0xffffffffUL; /* for WORDSIZE > 32 machines */ + i++; j++; + if (i>=N) { mt[0] = mt[N-1]; i=1; } + if (j>=key_length) j=0; + } + for (k=N-1; k; k--) { + mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * 1566083941UL)) + - i; /* non linear */ + mt[i] &= 0xffffffffUL; /* for WORDSIZE > 32 machines */ + i++; + if (i>=N) { mt[0] = mt[N-1]; i=1; } + } + + mt[0] = 0x80000000UL; /* MSB is 1; assuring non-zero initial array */ +} + +void RNG::seed(unsigned long s) +{ + mt[0]= s & 0xffffffffUL; + for (mti=1; mti> 30)) + mti); + /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */ + /* In the previous versions, MSBs of the seed affect */ + /* only MSBs of the array mt[]. */ + /* 2002/01/09 modified by Makoto Matsumoto */ + mt[mti] &= 0xffffffffUL; + /* for >32 bit machines */ + } +} + +/* generates a random number on [0,0xffffffff]-interval */ +unsigned long RNG::getInt32() +{ + unsigned long y; + static unsigned long mag01[2]={0x0UL, _MATRIX_A}; + /* mag01[x] = x * _MATRIX_A for x=0,1 */ + + if (mti >= N) { /* generate N words at one time */ + int kk; + + for (kk=0;kk> 1) ^ mag01[y & 0x1UL]; + } + for (;kk> 1) ^ mag01[y & 0x1UL]; + } + y = (mt[N-1]&_UPPER_MASK)|(mt[0]&_LOWER_MASK); + mt[N-1] = mt[_M-1] ^ (y >> 1) ^ mag01[y & 0x1UL]; + + mti = 0; + } + + y = mt[mti++]; + + /* Tempering */ + y ^= (y >> 11); + y ^= (y << 7) & 0x9d2c5680UL; + y ^= (y << 15) & 0xefc60000UL; + y ^= (y >> 18); + + return y; +} + +/* generates a random number on [0,0x7fffffff]-interval */ +long RNG::getInt31() +{ + return (long)(getInt32()>>1); +} + +/* generates a random number on [0,1]-real-interval */ +double RNG::getDoubleLR() +{ + return getInt32()*(1.0/4294967295.0); + /* divided by 2^32-1 */ +} + +/* generates a random number on [0,1)-real-interval */ +double RNG::getDoubleL() +{ + return getInt32()*(1.0/4294967296.0); + /* divided by 2^32 */ +} + +/* generates a random number on (0,1)-real-interval */ +double RNG::getDouble() +{ + return (((double)getInt32()) + 0.5)*(1.0/4294967296.0); + /* divided by 2^32 */ +} + +float RNG::getFloatLR() +{ + return getInt32()*(1.0f/4294967295.0f); + /* divided by 2^32-1 */ +} +float RNG::getFloatL() +{ + return getInt32()*(1.0f/4294967296.0f); + /* divided by 2^32 */ +} +float RNG::getFloat() +{ + return (getInt32() + 0.5f)*(1.0f/4294967296.0f); + /* divided by 2^32 */ +} + diff --git a/BlueNoise/RNG.h b/BlueNoise/RNG.h new file mode 100644 index 0000000..cd1ec2b --- /dev/null +++ b/BlueNoise/RNG.h @@ -0,0 +1,37 @@ +#include + +using namespace std; + +class RNG { +private: + /* Period parameters */ + static const long N = 624; + static const long _M = 397; + static const unsigned long _MATRIX_A = 0x9908b0dfUL; /* constant vector a */ + static const unsigned long _UPPER_MASK = 0x80000000UL; /* most significant w-r bits */ + static const unsigned long _LOWER_MASK = 0x7fffffffUL; /* least significant r bits */ + +private: + unsigned long mt[N]; /* the array for the state vector */ + int mti; + +public: + RNG(unsigned long seed=5489UL); + RNG(unsigned long *init_key, int key_length); + + void seed(unsigned long seed); + + /* generates a random number on [0,0xffffffff]-interval */ + unsigned long getInt32(); + /* generates a random number on [0,0x7fffffff]-interval */ + long getInt31(); + /* generates a random number on [0,1]-real-interval */ + double getDoubleLR(); + float getFloatLR(); + /* generates a random number on [0,1)-real-interval */ + double getDoubleL(); + float getFloatL(); + /* generates a random number on (0,1)-real-interval */ + double getDouble(); + float getFloat(); +}; diff --git a/BlueNoise/RangeList.cpp b/BlueNoise/RangeList.cpp new file mode 100644 index 0000000..741ea11 --- /dev/null +++ b/BlueNoise/RangeList.cpp @@ -0,0 +1,139 @@ +// $Id: RangeList.cpp,v 1.4 2006/01/24 03:22:14 zr Exp $ + +#define _USE_MATH_DEFINES +#include +#include +#include + +#include "RangeList.h" + +/// + +static const float kSmallestRange = .000001f; + +RangeList::RangeList(float min, float max) +{ + numRanges = 0; + rangesSize = 8; + ranges = new RangeEntry[rangesSize]; + reset(min, max); +} + +RangeList::~RangeList() +{ + delete[] ranges; +} + +void RangeList::reset(float min, float max) +{ + numRanges = 1; + ranges[0].min = min; + ranges[0].max = max; +} + +void RangeList::deleteRange(int pos) +{ + if (postwoPi) { + subtract(a-twoPi, b-twoPi); + } else if (b<0) { + subtract(a+twoPi, b+twoPi); + } else if (a<0) { + subtract(0, b); + subtract(a+twoPi,twoPi); + } else if (b>twoPi) { + subtract(a, twoPi); + subtract(0, b-twoPi); + } else if (numRanges==0) { + ; + } else { + int pos; + + if (a>1; + if (ranges[mid].minranges[pos+1].min) { + pos++; + } else { + return; + } + } + + while (posranges[pos].min) { + if (ranges[pos].max-b + +typedef struct _RangeEntry { + float min, max; +} RangeEntry; + +class RangeList { +public: + RangeEntry *ranges; + int numRanges, rangesSize; + +public: + RangeList(float min, float max); + ~RangeList(); + + void reset(float min, float max); + + void print(); + + void subtract(float min, float max); + +private: + void deleteRange(int pos); + void insertRange(int pos, float min, float max); +}; diff --git a/BlueNoise/ScallopedSector.cpp b/BlueNoise/ScallopedSector.cpp new file mode 100644 index 0000000..ac9bbc7 --- /dev/null +++ b/BlueNoise/ScallopedSector.cpp @@ -0,0 +1,289 @@ +#define _USE_MATH_DEFINES +#include +#include + +#include + +#include "BLUE_NOISE.h" +#include "ScallopedSector.h" + +static const float kTwoPi = (float) (M_PI*2); + +static float integralOfDistToCircle(float x, float d, float r, float k) +{ + if (r1) y = 1; + + float theta = asin(y); + + return (r*(r*(x + + k*theta) + + k*cos(theta)*d_sin_x) + + d*cos(x)*d_sin_x)*.5f; +} + +ScallopedSector::ScallopedSector(Vec2 &_Pt, float _a1, float _a2, Vec2 &P1, float r1, float sign1, Vec2 &P2, float r2, float sign2) +{ + Vec2 v1 = Vec2(P1.x - _Pt.x, P1.y - _Pt.y); + Vec2 v2 = Vec2(P2.x - _Pt.x, P2.y - _Pt.y); + + P = _Pt; + a1 = _a1; + a2 = _a2; + + arcs[0].P = P1; + arcs[0].r = r1; + arcs[0].sign = sign1; + arcs[0].d = sqrt(v1.x*v1.x + v1.y*v1.y); + arcs[0].rSqrd = arcs[0].r*arcs[0].r; + arcs[0].dSqrd = arcs[0].d*arcs[0].d; + arcs[0].theta = atan2(v1.y,v1.x); + arcs[0].integralAtStart = integralOfDistToCircle(a1 - arcs[0].theta, arcs[0].d, arcs[0].r, arcs[0].sign); + + arcs[1].P = P2; + arcs[1].r = r2; + arcs[1].sign = sign2; + arcs[1].d = sqrt(v2.x*v2.x + v2.y*v2.y); + arcs[1].rSqrd = arcs[1].r*arcs[1].r; + arcs[1].dSqrd = arcs[1].d*arcs[1].d; + arcs[1].theta = atan2(v2.y,v2.x); + arcs[1].integralAtStart = integralOfDistToCircle(a1 - arcs[1].theta, arcs[1].d, arcs[1].r, arcs[1].sign); + + area = calcAreaToAngle(a2); +} + +float ScallopedSector::calcAreaToAngle(float angle) +{ + float underInner = integralOfDistToCircle(angle - arcs[0].theta, arcs[0].d, arcs[0].r, arcs[0].sign) - arcs[0].integralAtStart; + float underOuter = integralOfDistToCircle(angle - arcs[1].theta, arcs[1].d, arcs[1].r, arcs[1].sign) - arcs[1].integralAtStart; + + return underOuter-underInner; +} + +float ScallopedSector::calcAngleForArea(float area, RNG &rng) +{ + float lo = a1, hi = a2, cur = lo + (hi-lo)*rng.getFloat(); + + for (int i=0; i<10; i++) { + if (calcAreaToAngle(cur) *regions) +{ + std::vector angles; + + Vec2 v(C.x - P.x, C.y-P.y); + float d = sqrt(v.x*v.x + v.y*v.y); + + if (rFLT_EPSILON) { + float invD = 1.0f/d; + float x = (d*d - r*r + R*R)*invD*.5f; + float k = R*R - x*x; + + if (k>0) { + float y = sqrt(k); + float vx = v.x*invD; + float vy = v.y*invD; + float vx_x = vx*x, vy_x = vy*x; + float vx_y = vx*y, vy_y = vy*y; + float angle; + + angle = canonizeAngle(atan2(C2.y + vy_x + vx_y - P.y, + C2.x + vx_x - vy_y - P.x)); + if (a1outer) { + regions->push_back(ScallopedSector(P, a1, a2, arcs[0].P, arcs[0].r, arcs[0].sign, arcs[1].P, arcs[1].r, arcs[1].sign)); + } else { + if (innerpush_back(ScallopedSector(P, a1, a2, arcs[0].P, arcs[0].r, arcs[0].sign, C, r, -1)); + } + if (d2push_back(ScallopedSector(P, a1, a2, C, r, 1, arcs[1].P, arcs[1].r, arcs[1].sign)); + } + } + } +} + +/// + +ScallopedRegion::ScallopedRegion(Vec2 &P, float r1, float r2, float _minArea) : + minArea(_minArea) +{ + regions = new std::vector; + regions->push_back(ScallopedSector(P, 0, kTwoPi, P, r1, 1, P, r2, 1)); + area = (*regions)[0].area; +} + +ScallopedRegion::~ScallopedRegion() +{ + delete regions; +} + +void ScallopedRegion::subtractDisk(Vec2 C, float r) +{ + std::vector *newRegions = new std::vector; + + area = 0; + for (unsigned int i=0; isize(); i++) { + ScallopedSector &ss = (*regions)[i]; + std::vector *tmp = new std::vector; + + ss.subtractDisk(C, r, tmp); + + for (unsigned int j=0; jsize(); j++) { + ScallopedSector &nss = (*tmp)[j]; + + if (nss.area>minArea) { + area += nss.area; + + if (newRegions->size()) { + ScallopedSector &last = (*newRegions)[newRegions->size()-1]; + if (last.a2==nss.a1 && (last.arcs[0].P==nss.arcs[0].P && last.arcs[0].r==nss.arcs[0].r && last.arcs[0].sign==nss.arcs[0].sign) && + (last.arcs[1].P==nss.arcs[1].P && last.arcs[1].r==nss.arcs[1].r && last.arcs[1].sign==nss.arcs[1].sign)) { + last.a2 = nss.a2; + last.area = last.calcAreaToAngle(last.a2); + continue; + } + } + + newRegions->push_back(nss); + } + } + + delete tmp; + } + + delete regions; + regions = newRegions; +} + +Vec2 ScallopedRegion::sample(RNG &rng) +{ + if (!regions->size()) { + printf("Fatal error, sampled from empty region."); + exit(1); + return Vec2(0,0); + } else { + float a = area*rng.getFloatL(); + ScallopedSector &ss = (*regions)[0]; + + for (unsigned int i=0; isize(); i++) { + ss = (*regions)[i]; + if (a + +typedef struct { + Vec2 P; + float r, sign, d, theta, integralAtStart; + float rSqrd, dSqrd; +} ArcData; + +class ScallopedSector +{ +public: + Vec2 P; + float a1, a2, area; + + ArcData arcs[2]; + +public: + ScallopedSector(Vec2 &_Pt, float _a1, float _a2, Vec2 &P1, float r1, float sign1, Vec2 &P2, float r2, float sign2); + + float calcAreaToAngle(float angle); + float calcAngleForArea(float area, RNG &rng); + Vec2 sample(RNG &rng); + + float distToCurve(float angle, int index); + + void subtractDisk(Vec2 &C, float r, std::vector *regions); + +private: + float canonizeAngle(float angle); + + void distToCircle(float angle, Vec2 &C, float r, float *d1_out, float *d2_out); +}; + +class ScallopedRegion +{ +public: + std::vector *regions; + float minArea; + float area; + +public: + ScallopedRegion(Vec2 &P, float r1, float r2, float minArea=.00000001); + ~ScallopedRegion(); + + bool isEmpty() { return regions->size()==0; } + void subtractDisk(Vec2 C, float r); + + Vec2 sample(RNG &rng); +}; diff --git a/BlueNoise/WeightedDiscretePDF.cpp b/BlueNoise/WeightedDiscretePDF.cpp new file mode 100644 index 0000000..f9bde2f --- /dev/null +++ b/BlueNoise/WeightedDiscretePDF.cpp @@ -0,0 +1,313 @@ +// $Id: WeightedDiscretePDF.cpp,v 1.4 2006/07/07 05:54:31 zr Exp $ + +#include +#include "WeightedDiscretePDF.h" + +// + +template +WDPDF_Node::WDPDF_Node(T key_, float weight_, WDPDF_Node *parent_) +{ + m_mark = false; + + key = key_; + weight = weight_; + sumWeights = 0; + left = right = 0; + parent = parent_; +} + +template +WDPDF_Node::~WDPDF_Node() +{ + if (left) delete left; + if (right) delete right; +} + +// + +template +WeightedDiscretePDF::WeightedDiscretePDF() +{ + m_root = 0; +} + +template +WeightedDiscretePDF::~WeightedDiscretePDF() +{ + if (m_root) delete m_root; +} + +template +void WeightedDiscretePDF::insert(T item, float weight) +{ + WDPDF_Node *p=0, *n=m_root; + + while (n) { + if (n->leftIsRed() && n->rightIsRed()) + split(n); + + p = n; + if (n->key==item) { + throw std::domain_error("insert: argument(item) already in tree"); + } else { + n = (itemkey)?n->left:n->right; + } + } + + n = new WDPDF_Node(item, weight, p); + + if (!p) { + m_root = n; + } else { + if (itemkey) { + p->left = n; + } else { + p->right = n; + } + + split(n); + } + + propogateSumsUp(n); +} + +template +void WeightedDiscretePDF::remove(T item) +{ + WDPDF_Node **np = lookup(item, 0); + WDPDF_Node *child, *n = *np; + + if (!n) { + throw std::domain_error("remove: argument(item) not in tree"); + } else { + if (n->left) { + WDPDF_Node **leftMaxp = &n->left; + + while ((*leftMaxp)->right) + leftMaxp = &(*leftMaxp)->right; + + n->key = (*leftMaxp)->key; + n->weight = (*leftMaxp)->weight; + + np = leftMaxp; + n = *np; + } + + // node now has at most one child + + child = n->left?n->left:n->right; + *np = child; + + if (child) { + child->parent = n->parent; + + if (n->isBlack()) { + lengthen(child); + } + } + + propogateSumsUp(n->parent); + + n->left = n->right = 0; + delete n; + } +} + +template +void WeightedDiscretePDF::update(T item, float weight) +{ + WDPDF_Node *n = *lookup(item, 0); + + if (!n) { + throw std::domain_error("update: argument(item) not in tree"); + } else { + float delta = weight - n->weight; + n->weight = weight; + + for (; n; n=n->parent) { + n->sumWeights += delta; + } + } +} + +template +T WeightedDiscretePDF::choose(float p) +{ + if (p<0.0 || p>=1.0) { + throw std::domain_error("choose: argument(p) outside valid range"); + } else if (!m_root) { + throw std::logic_error("choose: choose() called on empty tree"); + } else { + float w = m_root->sumWeights * p; + WDPDF_Node *n = m_root; + + while (1) { + if (n->left) { + if (wleft->sumWeights) { + n = n->left; + continue; + } else { + w -= n->left->sumWeights; + } + } + if (wweight || !n->right) { + break; // !n->right condition shouldn't be necessary, just sanity check + } + w -= n->weight; + n = n->right; + } + + return n->key; + } +} + +template +bool WeightedDiscretePDF::inTree(T item) +{ + WDPDF_Node *n = *lookup(item, 0); + + return !!n; +} + +// + +template +WDPDF_Node **WeightedDiscretePDF::lookup(T item, WDPDF_Node **parent_out) +{ + WDPDF_Node *n, *p=0, **np=&m_root; + + while ((n = *np)) { + if (n->key==item) { + break; + } else { + p = n; + if (itemkey) { + np = &n->left; + } else { + np = &n->right; + } + } + } + + if (parent_out) + *parent_out = p; + return np; +} + +template +void WeightedDiscretePDF::split(WDPDF_Node *n) +{ + if (n->left) n->left->markBlack(); + if (n->right) n->right->markBlack(); + + if (n->parent) { + WDPDF_Node *p = n->parent; + + n->markRed(); + + if (p->isRed()) { + p->parent->markRed(); + + // not same direction + if (!( (n==p->left && p==p->parent->left) || + (n==p->right && p==p->parent->right))) { + rotate(n); + p = n; + } + + rotate(p); + p->markBlack(); + } + } +} + +template +void WeightedDiscretePDF::rotate(WDPDF_Node *n) +{ + WDPDF_Node *p=n->parent, *pp=p->parent; + + n->parent = pp; + p->parent = n; + + if (n==p->left) { + p->left = n->right; + n->right = p; + if (p->left) p->left->parent = p; + } else { + p->right = n->left; + n->left = p; + if (p->right) p->right->parent = p; + } + + n->setSum(); + p->setSum(); + + if (!pp) { + m_root = n; + } else { + if (p==pp->left) { + pp->left = n; + } else { + pp->right = n; + } + } +} + +template +void WeightedDiscretePDF::lengthen(WDPDF_Node *n) +{ + if (n->isRed()) { + n->markBlack(); + } else if (n->parent) { + WDPDF_Node *sibling = n->sibling(); + + if (sibling && sibling->isRed()) { + n->parent->markRed(); + sibling->markBlack(); + + rotate(sibling); // node sibling is now old sibling child, must be black + sibling = n->sibling(); + } + + // sibling is black + + if (!sibling) { + lengthen(n->parent); + } else if (sibling->leftIsBlack() && sibling->rightIsBlack()) { + if (n->parent->isBlack()) { + sibling->markRed(); + lengthen(n->parent); + } else { + sibling->markRed(); + n->parent->markBlack(); + } + } else { + if (n==n->parent->left && sibling->rightIsBlack()) { + rotate(sibling->left); // sibling->left must be red + sibling->markRed(); + sibling->parent->markBlack(); + sibling = sibling->parent; + } else if (n==n->parent->right && sibling->leftIsBlack()) { + rotate(sibling->right); // sibling->right must be red + sibling->markRed(); + sibling->parent->markBlack(); + sibling = sibling->parent; + } + + // sibling is black, and sibling's far child is red + + rotate(sibling); + if (n->parent->isRed()) sibling->markRed(); + sibling->left->markBlack(); + sibling->right->markBlack(); + } + } +} + +template +void WeightedDiscretePDF::propogateSumsUp(WDPDF_Node *n) +{ + for (; n; n=n->parent) + n->setSum(); +} diff --git a/BlueNoise/WeightedDiscretePDF.h b/BlueNoise/WeightedDiscretePDF.h new file mode 100644 index 0000000..8705ece --- /dev/null +++ b/BlueNoise/WeightedDiscretePDF.h @@ -0,0 +1,57 @@ +// $Id: WeightedDiscretePDF.h,v 1.4 2006/07/07 05:54:31 zr Exp $ + +template +class WDPDF_Node +{ +private: + bool m_mark; + +public: + WDPDF_Node *parent, *left, *right; + T key; + float weight, sumWeights; + +public: + WDPDF_Node(T key_, float weight_, WDPDF_Node *parent_); + ~WDPDF_Node(); + + WDPDF_Node *sibling() { return this==parent->left?parent->right:parent->left; } + + void markRed() { m_mark = true; } + void markBlack() { m_mark = false; } + bool isRed() { return m_mark; } + bool isBlack() { return !m_mark; } + bool leftIsBlack() { return !left || left->isBlack(); } + bool rightIsBlack() { return !right || right->isBlack(); } + bool leftIsRed() { return !leftIsBlack(); } + bool rightIsRed() { return !rightIsBlack(); } + void setSum() { sumWeights = weight + (left?left->sumWeights:0) + (right?right->sumWeights:0); } +}; + +template +class WeightedDiscretePDF +{ +private: + WDPDF_Node *m_root; + +public: + WeightedDiscretePDF(); + ~WeightedDiscretePDF(); + + void insert(T item, float weight); + void update(T item, float newWeight); + void remove(T item); + bool inTree(T item); + + /* pick a tree element according to its + * weight. p should be in [0,1). + */ + T choose(float p); + +private: + WDPDF_Node **lookup(T item, WDPDF_Node **parent_out); + void split(WDPDF_Node *node); + void rotate(WDPDF_Node *node); + void lengthen(WDPDF_Node *node); + void propogateSumsUp(WDPDF_Node *n); +}; diff --git a/CELL.cpp b/CELL.cpp new file mode 100644 index 0000000..c8067c2 --- /dev/null +++ b/CELL.cpp @@ -0,0 +1,225 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : CELL.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#include "CELL.h" + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +// normal cell constructor +CELL::CELL(float north, float east, float south, float west, CELL* parent, int depth) : + parent(parent), depth(depth), index(-1), candidate(false), + boundary(false), potential(0.0f), state(EMPTY) +{ + for (int x = 0; x < 4; x++) + children[x] = NULL; + for (int x = 0; x < 8; x++) + neighbors[x] = NULL; + + bounds[0] = north; bounds[1] = east; bounds[2] = south; bounds[3] = west; + + center[0] = (bounds[1] + bounds[3]) * 0.5f; + center[1] = (bounds[0] + bounds[2]) * 0.5f; +} + +// ghost cell constructor +CELL::CELL(int depth) : parent(NULL), depth(depth), index(-1), candidate(false), + boundary(true), potential(0.0f), state(EMPTY) +{ + for (int x = 0; x < 4; x++) + children[x] = NULL; + for (int x = 0; x < 8; x++) + neighbors[x] = NULL; + + bounds[0] = 0.0f; bounds[1] = 0.0f; bounds[2] = 0.0f; bounds[3] = 0.0f; + center[0] = 0.0f; center[1] = 0.0f; +} + +CELL::~CELL() { + int x; + + for (x = 0; x < 4; x++) + if (children[x] != NULL) + { + delete children[x]; + children[x] = NULL; + } +} + +////////////////////////////////////////////////////////////////////// +// refine current cell +////////////////////////////////////////////////////////////////////// +void CELL::refine() { + if (children[0] != NULL) return; + float center[] = {(bounds[0] + bounds[2]) * 0.5f, (bounds[1] + bounds[3]) * 0.5f}; + + children[0] = new CELL(bounds[0], center[1], center[0], bounds[3], this, depth + 1); + children[1] = new CELL(bounds[0], bounds[1], center[0], center[1], this, depth + 1); + children[2] = new CELL(center[0], bounds[1], bounds[2], center[1], this, depth + 1); + children[3] = new CELL(center[0], center[1], bounds[2], bounds[3], this, depth + 1); + + children[0]->potential = potential; + children[1]->potential = potential; + children[2]->potential = potential; + children[3]->potential = potential; +} + +////////////////////////////////////////////////////////////////////// +// return north neighbor to current cell +////////////////////////////////////////////////////////////////////// +CELL* CELL::northNeighbor() +{ + // if it is the root + if (this->parent == NULL) return NULL; + + // if it is the southern child of the parent + if (parent->children[3] == this) return parent->children[0]; + if (parent->children[2] == this) return parent->children[1]; + + // else look up higher + CELL* mu = parent->northNeighbor(); + + // if there are no more children to look at, + // this is the answer + if (mu == NULL || mu->children[0] == NULL) return mu; + // if it is the NW child of the parent + else if (parent->children[0] == this) return mu->children[3]; + // if it is the NE child of the parent + else return mu->children[2]; +} + +////////////////////////////////////////////////////////////////////// +// return north neighbor to current cell +////////////////////////////////////////////////////////////////////// +CELL* CELL::southNeighbor() +{ + // if it is the root + if (this->parent == NULL) return NULL; + + // if it is the northern child of the parent + if (parent->children[0] == this) return parent->children[3]; + if (parent->children[1] == this) return parent->children[2]; + + // else look up higher + CELL* mu = parent->southNeighbor(); + + // if there are no more children to look at, + // this is the answer + if (mu == NULL || mu->children[0] == NULL) return mu; + // if it is the SW child of the parent + else if (parent->children[3] == this) return mu->children[0]; + // if it is the SE child of the parent + else return mu->children[1]; +} + +////////////////////////////////////////////////////////////////////// +// return north neighbor to current cell +////////////////////////////////////////////////////////////////////// +CELL* CELL::westNeighbor() +{ + // if it is the root + if (this->parent == NULL) return NULL; + + // if it is the eastern child of the parent + if (parent->children[1] == this) return parent->children[0]; + if (parent->children[2] == this) return parent->children[3]; + + // else look up higher + CELL* mu = parent->westNeighbor(); + + // if there are no more children to look at, + // this is the answer + if (mu == NULL || mu->children[0] == NULL) return mu; + // if it is the NW child of the parent + else if (parent->children[0] == this) return mu->children[1]; + // if it is the SW child of the parent + else return mu->children[2]; +} + +////////////////////////////////////////////////////////////////////// +// return north neighbor to current cell +////////////////////////////////////////////////////////////////////// +CELL* CELL::eastNeighbor() +{ + // if it is the root + if (this->parent == NULL) return NULL; + + // if it is the western child of the parent + if (parent->children[0] == this) return parent->children[1]; + if (parent->children[3] == this) return parent->children[2]; + + // else look up higher + CELL* mu = parent->eastNeighbor(); + + // if there are no more children to look at, + // this is the answer + if (mu == NULL || mu->children[0] == NULL) return mu; + // if it is the NE child of the parent + else if (parent->children[1] == this) return mu->children[0]; + // if it is the SE child of the parent + else return mu->children[3]; +} + diff --git a/CELL.h b/CELL.h new file mode 100644 index 0000000..27256ed --- /dev/null +++ b/CELL.h @@ -0,0 +1,194 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : CELL.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#ifndef CELL_H +#define CELL_H +#include + +////////////////////////////////////////////////////////////////////// +/// \enum Possible states of the cell in the DBM simulation +////////////////////////////////////////////////////////////////////// +enum CELL_STATE {EMPTY, NEGATIVE, POSITIVE, REPULSOR, ATTRACTOR}; + +////////////////////////////////////////////////////////////////////// +/// \brief Basic cell data structure of the quadtree +////////////////////////////////////////////////////////////////////// +class CELL +{ +public: + //! normal cell constructor + CELL(float north, + float east, + float south, + float west, + CELL* parent = NULL, + int depth = 0); + + //! ghost cell constructor + CELL(int depth = 0); + + //! destructor + ~CELL(); + + //! The children of the node in the quadtree + /*! + Winding order of children is: + + \verbatim + _________ + | | | + | 0 | 1 | + |___|___| + | | | + | 3 | 2 | + |___|___| + \endverbatim */ + CELL* children[4]; + + //! The physical bounds of the current grid cell + /*! + Winding order of bounds is: + + \verbatim + 0 - north + 1 - east + 2 - south + 3 - west + \endverbatim */ + float bounds[4]; + + //! The neighbors in the balanced quadtree + /*! + winding order of the neighbors is: + + \verbatim + | 0 | 1 | + ____|____|____|_____ + | | + 7 | | 2 + ____| |_____ + | | + 6 | | 3 + ____|_________|_____ + | | | + | 5 | 4 | + \endverbatim + + Neighbors 0,2,4,6 should always exist. Depending on + if the neighbor is on a lower refinement level, + neighbors 1,3,5,7 may or may not exist. If they are not + present, the pointer value should ne NULL. */ + CELL* neighbors[8]; + + //! Poisson stencil coefficients + /*! + winding order of the stencil coefficients: + + \verbatim + | 0 | 1 | + ____|____|____|_____ + | | + 7 | | 2 + ____| 8 |_____ + | | + 6 | | 3 + ____|_________|_____ + | | | + | 5 | 4 | + \endverbatim + Stencils 0,2,4,6 should always exist. Depending on + if the neighbor is on a lower refinement level, + stencils 1,3,5,7 may or may not exist. If they are not + present, the pointer value should ne NULL. */ + float stencil[9]; + + float center[2]; ///< center of the cell + int depth; ///< current tree depth + bool candidate; ///< already a member of candidate list? + + CELL* parent; ///< parent node in the quadtree + CELL_STATE state; ///< DBM state of the cell + + void refine(); ///< subdivide the cell + + //////////////////////////////////////////////////////////////// + // solver-related variables + //////////////////////////////////////////////////////////////// + bool boundary; ///< boundary node to include in the solver? + float potential; ///< current electric potential + float b; ///< rhs of the linear system + float residual; ///< residual in the linear solver + int index; ///< lexicographic index for the solver + + //////////////////////////////////////////////////////////////// + // neighbor lookups + //////////////////////////////////////////////////////////////// + CELL* northNeighbor(); ///< lookup northern neighbor + CELL* southNeighbor(); ///< lookup southern neighbor + CELL* westNeighbor(); ///< lookup western neighbor + CELL* eastNeighbor(); ///< lookup eastern neighbor +}; + +#endif diff --git a/CG_SOLVER.cpp b/CG_SOLVER.cpp new file mode 100644 index 0000000..6260c34 --- /dev/null +++ b/CG_SOLVER.cpp @@ -0,0 +1,309 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : CG_SOLVER.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#include "CG_SOLVER.h" + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +CG_SOLVER::CG_SOLVER(int maxDepth, int iterations, int digits) : + _iterations(iterations), + _arraySize(0), _listSize(0), _digits(digits), + _direction(NULL), _residual(NULL), _q(NULL), _potential(NULL) +{ + // compute the physical size of various grid cells + _dx = new float[maxDepth + 1]; + _dx[0] = 1.0f; + for (int x = 1; x <= maxDepth; x++) + _dx[x] = _dx[x - 1] * 0.5f; +} + +CG_SOLVER::~CG_SOLVER() +{ + if (_direction) delete[] _direction; + if (_residual) delete[] _residual; + if (_potential) delete[] _potential; + if (_q) delete[] _q; +} + +////////////////////////////////////////////////////////////////////// +// reallocate scratch arrays if necessary +////////////////////////////////////////////////////////////////////// +void CG_SOLVER::reallocate() +{ + // if we have enough size already, return + if (_arraySize >= _listSize) return; + + // made sure it SSE aligns okay + _arraySize = _listSize * 2; + if (_arraySize % 4) + _arraySize += 4 - _arraySize % 4; + + // delete the old ones + if (_direction) delete[] _direction; + if (_residual) delete[] _residual; + if (_q) delete[] _q; + + // allocate the new ones + _direction = new float[_arraySize]; + _residual = new float[_arraySize]; + _q = new float[_arraySize]; + + // wipe the new ones + for (int x = 0; x < _arraySize; x++) + _direction[x] = _residual[x] = _q[x] = 0.0f; +} + +////////////////////////////////////////////////////////////////////// +// conjugate gradient solver +////////////////////////////////////////////////////////////////////// +int CG_SOLVER::solve(list cells) +{ + // counters + int x, y, index; + list::iterator cellIterator; + + // i = 0 + int i = 0; + + // precalculate stencils + calcStencils(cells); + + // reallocate scratch arrays if necessary + _listSize = cells.size(); + reallocate(); + + // compute a new lexicographical order + cellIterator = cells.begin(); + for (x = 0; x < _listSize; x++, cellIterator++) + (*cellIterator)->index = x; + + // r = b - Ax + calcResidual(cells); + + // copy residual into easy array + // d = r + cellIterator = cells.begin(); + float deltaNew = 0.0f; + for (x = 0; x < _listSize; x++, cellIterator++) + { + _direction[x] = _residual[x]; + deltaNew += _residual[x] * _residual[x]; + } + + // delta0 = deltaNew + float delta0 = deltaNew; + + // While deltaNew > (eps^2) * delta0 + float eps = pow(10.0f, (float)-_digits); + float maxR = 2.0f * eps; + while ((i < _iterations) && (maxR > eps)) + { + // q = Ad + cellIterator = cells.begin(); + for (y = 0; y < _listSize; y++, cellIterator++) + { + CELL* currentCell = *cellIterator; + CELL** neighbors = currentCell->neighbors; + + float neighborSum = 0.0f; + for (int x = 0; x < 4; x++) + { + int j = x * 2; + neighborSum += _direction[neighbors[j]->index] * currentCell->stencil[j]; + if (neighbors[j+1]) + neighborSum += _direction[neighbors[j+1]->index] * currentCell->stencil[j+1]; + } + _q[y] = -neighborSum + _direction[y] * currentCell->stencil[8]; + } + // alpha = deltaNew / (transpose(d) * q) + float alpha = 0.0f; + for (x = 0; x < _listSize; x++) + alpha += _direction[x] * _q[x]; + if (fabs(alpha) > 0.0f) + alpha = deltaNew / alpha; + + // x = x + alpha * d + cellIterator = cells.begin(); + for (x = 0; x < _listSize; x++, cellIterator++) + (*cellIterator)->potential += alpha * _direction[x]; + + // r = r - alpha * q + maxR = 0.0f; + for (x = 0; x < _listSize; x++) + { + _residual[x] -= _q[x] * alpha; + maxR = (_residual[x] > maxR) ? _residual[x] : maxR; + } + + // deltaOld = deltaNew + float deltaOld = deltaNew; + + // deltaNew = transpose(r) * r + deltaNew = 0.0f; + for (x = 0; x < _listSize; x++) + deltaNew += _residual[x] * _residual[x]; + + // beta = deltaNew / deltaOld + float beta = deltaNew / deltaOld; + + // d = r + beta * d + for (x = 0; x < _listSize; x++) + _direction[x] = _residual[x] + beta * _direction[x]; + + // i = i + 1 + i++; + } + + return i; +} + +////////////////////////////////////////////////////////////////////// +// calculate the residuals +////////////////////////////////////////////////////////////////////// +float CG_SOLVER::calcResidual(list cells) +{ + float maxResidual = 0.0f; + + list::iterator cellIterator = cells.begin(); + for (int i = 0; i < _listSize; i++, cellIterator++) + { + CELL* currentCell = *cellIterator; + float dx = _dx[currentCell->depth]; + float neighborSum = 0.0f; + + for (int x = 0; x < 4; x++) + { + int i = x * 2; + neighborSum += currentCell->neighbors[i]->potential * currentCell->stencil[i]; + if (currentCell->neighbors[i+1]) + neighborSum += currentCell->neighbors[i+1]->potential * currentCell->stencil[i+1]; + } + _residual[i] = currentCell->b - (-neighborSum + currentCell->potential * currentCell->stencil[8]); + + if (fabs(_residual[i]) > maxResidual) + maxResidual = fabs(_residual[i]); + } + return maxResidual; +} + +////////////////////////////////////////////////////////////////////// +// compute stencils once and store +////////////////////////////////////////////////////////////////////// +void CG_SOLVER::calcStencils(list cells) +{ + list::iterator cellIterator = cells.begin(); + for (cellIterator = cells.begin(); cellIterator != cells.end(); cellIterator++) + { + CELL* currentCell = *cellIterator; + float invDx = 1.0f / _dx[currentCell->depth]; + + // sum over faces + float deltaSum = 0.0f; + float bSum = 0.0f; + + for (int x = 0; x < 4; x++) + { + int i = x * 2; + currentCell->stencil[i] = 0.0f; + currentCell->stencil[i+1] = 0.0f; + + if (currentCell->neighbors[i + 1] == NULL) { + // if it is the same refinement level (case 1) + if (currentCell->depth == currentCell->neighbors[i]->depth) { + deltaSum += invDx; + if (!currentCell->neighbors[i]->boundary) + currentCell->stencil[i] = invDx; + else + bSum += (currentCell->neighbors[i]->potential) * invDx; + } + // else it is less refined (case 3) + else { + deltaSum += 0.5f * invDx; + if (!currentCell->neighbors[i]->boundary) + currentCell->stencil[i] = 0.5f * invDx; + else + bSum += currentCell->neighbors[i]->potential * 0.5f * invDx; + } + } + // if the neighbor is at a lower level (case 2) + else { + deltaSum += 2.0f * invDx; + if (!currentCell->neighbors[i]->boundary) + currentCell->stencil[i] = invDx; + else + bSum += currentCell->neighbors[i]->potential * invDx; + if (!currentCell->neighbors[i+1]->boundary) + currentCell->stencil[i+1] = invDx; + else + bSum += currentCell->neighbors[i+1]->potential * invDx; + } + } + + currentCell->stencil[8] = deltaSum; + currentCell->b = bSum; + } +} diff --git a/CG_SOLVER.h b/CG_SOLVER.h new file mode 100644 index 0000000..add6e2f --- /dev/null +++ b/CG_SOLVER.h @@ -0,0 +1,120 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : CG_SOLVER.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#ifndef CG_SOLVER_H +#define CG_SOLVER_H + +#include "CELL.h" +#include +#include + +using namespace std; + +//////////////////////////////////////////////////////////////////// +/// \brief Conjugate gradient Poisson solver. +//////////////////////////////////////////////////////////////////// +class CG_SOLVER +{ +public: + //! constructor + CG_SOLVER(int maxDepth, int iterations = 10, int digits = 8); + //! destructor + virtual ~CG_SOLVER(); + + //! solve the Poisson problem + virtual int solve(list cells); + + //! calculate the residual + float calcResidual(list cells); + + //! accessor for the maximum number of iterations + int& iterations() { return _iterations; }; + +protected: + int _iterations; ///< maximum number of iterations + int _digits; ///< desired digits of precision + + //////////////////////////////////////////////////////////////// + // conjugate gradient arrays + //////////////////////////////////////////////////////////////// + float* _direction; ///< conjugate gradient 'd' array + float* _potential; ///< conjugate gradient solution, 'x' array + float* _residual; ///< conjugate gradient residual, 'r' array + float* _q; ///< conjugate gradient 'q' array + + int _arraySize; ///< currently allocated array size + int _listSize; ///< current system size + + //! compute stencils once and store + void calcStencils(list cells); + + //! reallocate the scratch arrays + virtual void reallocate(); + + //! physical lengths of various cell sizes + float* _dx; +}; + +#endif diff --git a/CG_SOLVER_SSE.cpp b/CG_SOLVER_SSE.cpp new file mode 100644 index 0000000..fa34894 --- /dev/null +++ b/CG_SOLVER_SSE.cpp @@ -0,0 +1,416 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : CG_SOLVER_SSE.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#include "CG_SOLVER_SSE.h" + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +CG_SOLVER_SSE::CG_SOLVER_SSE(int maxDepth, int iterations, int digits) : + CG_SOLVER(maxDepth, iterations, digits) +{ +} + +CG_SOLVER_SSE::~CG_SOLVER_SSE() +{ + if (_direction) free(_direction); + if (_potential) free(_potential); + if (_residual) free(_residual); + if (_q) free(_q); + + _direction = NULL; + _residual = NULL; + _q = NULL; + _potential = NULL; +} + +////////////////////////////////////////////////////////////////////// +// reallocate the sse arrays +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::reallocate() +{ + if (_arraySize >= _listSize) return; + _arraySize = _listSize * 2; + + if (_arraySize % 4) + _arraySize += 4 - _arraySize % 4; + + if (_direction) free(_direction); + if (_potential) free(_potential); + if (_residual) free(_residual); + if (_q) free(_q); + + _direction = (float*) aligned_alloc(_arraySize * sizeof(float), 16); + _potential = (float*) aligned_alloc(_arraySize * sizeof(float), 16); + _residual = (float*) aligned_alloc(_arraySize * sizeof(float), 16); + _q = (float*) aligned_alloc(_arraySize * sizeof(float), 16); + + return; +} + +////////////////////////////////////////////////////////////////////// +// solve the linear system +////////////////////////////////////////////////////////////////////// +int CG_SOLVER_SSE::solve(list cells) +{ + // counters + int x, y, index; + list::iterator cellIterator; + + // i = 0 + int i = 0; + + // precalculate stencils + calcStencils(cells); + + // reallocate scratch arrays if necessary + _listSize = cells.size(); + reallocate(); + wipeSSE(_potential); + wipeSSE(_direction); + wipeSSE(_residual); + wipeSSE(_q); + + // compute a new lexicographical order + cellIterator = cells.begin(); + for (x = 0; x < _listSize; x++, cellIterator++) + { + CELL* cell = *cellIterator; + cell->index = x; + _potential[x] = cell->potential; + } + + // r = b - Ax + calcResidual(cells); + + // d = r + copySSE(_direction, _residual); + + // deltaNew = r^T r + float deltaNew = dotSSE(_residual, _residual); + + // delta0 = deltaNew + float delta0 = deltaNew; + + // While deltaNew > (eps^2) * delta0 + float eps = pow(10.0f, (float)-_digits); + float maxR = 2.0f * eps; + while ((i < _iterations) && (maxR > eps)) + { + // q = Ad + cellIterator = cells.begin(); + for (y = 0; y < _listSize; y++, cellIterator++) + { + CELL* currentCell = *cellIterator; + CELL** neighbors = currentCell->neighbors; + float* stencil = currentCell->stencil; + + float neighborSum = 0.0f; + for (int x = 0; x < 8; x++) + { + if (neighbors[x]) + neighborSum += _direction[neighbors[x]->index] * stencil[x]; + } + _q[y] = -neighborSum + _direction[y] * currentCell->stencil[8]; + } + + // alpha = deltaNew / (transpose(d) * q) + float alpha = dotSSE(_q, _direction); + if (fabs(alpha) > 0.0f) + alpha = deltaNew / alpha; + + // x = x + alpha * d + saxpySSE(alpha, _direction, _potential); + + // r = r - alpha * q + saxpySSE(-alpha, _q, _residual); + maxR = maxSSE(_residual); + + // deltaOld = deltaNew + float deltaOld = deltaNew; + + // deltaNew = transpose(r) * r + deltaNew = dotSSE(_residual, _residual); + + // beta = deltaNew / deltaOld + float beta = deltaNew / deltaOld; + + // d = r + beta * d + saypxSSE(beta, _residual, _direction); + + // i = i + 1 + i++; + } + + // copy back into the tree + cellIterator = cells.begin(); + for (x = 0; x < _listSize; x++, cellIterator++) + (*cellIterator)->potential = _potential[x]; + + return i; +} + +////////////////////////////////////////////////////////////////////// +// dot product of two vectors +////////////////////////////////////////////////////////////////////// +float CG_SOLVER_SSE::dotSSE(float* x, float* y) +{ + __m128 sum = _mm_set_ps1(0.0f); + __m128* xSSE = (__m128*)x; + __m128* ySSE = (__m128*)y; + __m128 temp; + for (int index = 0; index < _arraySize / 4; index++) + { + temp = _mm_mul_ps(*xSSE, *ySSE); + sum = _mm_add_ps(sum, temp); + xSSE++; + ySSE++; + } + union u { + __m128 m; + float f[4]; + } extract; + extract.m = sum; + return extract.f[0] + extract.f[1] + extract.f[2] + extract.f[3]; +} + +////////////////////////////////////////////////////////////////////// +// scalar 'a' x + y +// Y = aX + Y +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::saxpySSE(float s, float* x, float* y) +{ + __m128* ySSE = (__m128*)y; + __m128* xSSE = (__m128*)x; + __m128 sSSE = _mm_set_ps1(s); + __m128 temp; + for (int index = 0; index < _arraySize / 4; index++) + { + temp = _mm_mul_ps(*xSSE, sSSE); + *ySSE = _mm_add_ps(*ySSE, temp); + + xSSE++; + ySSE++; + } +} + +////////////////////////////////////////////////////////////////////// +// scalar 'a' y + x +// Y = aY + X +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::saypxSSE(float s, float* x, float* y) +{ + __m128* ySSE = (__m128*)y; + __m128* xSSE = (__m128*)x; + __m128 sSSE = _mm_set_ps1(s); + __m128 temp; + for (int index = 0; index < _arraySize / 4; index++) + { + temp = _mm_mul_ps(*ySSE, sSSE); + *ySSE = _mm_add_ps(*xSSE, temp); + + xSSE++; + ySSE++; + } +} + +////////////////////////////////////////////////////////////////////// +// scalar 'a' y + x +// Y = aY + X +////////////////////////////////////////////////////////////////////// +float CG_SOLVER_SSE::maxSSE(float* x) +{ + __m128 maxFoundSSE= _mm_set_ps1(0.0f); + __m128* xSSE = (__m128*)x; + for (int index = 0; index < _arraySize / 4; index++) + { + maxFoundSSE = _mm_max_ps(*xSSE, maxFoundSSE); + xSSE++; + } + union u { + __m128 m; + float f[4]; + } extract; + extract.m = maxFoundSSE; + float maxFound = extract.f[0] > extract.f[1] ? extract.f[0] : extract.f[1]; + maxFound = maxFound > extract.f[2] ? maxFound : extract.f[2]; + return maxFound > extract.f[3] ? maxFound : extract.f[3]; +} + +////////////////////////////////////////////////////////////////////// +// SSE add +// Y = X + Y +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::addSSE(float* x, float* y) +{ + __m128* ySSE = (__m128*)y; + __m128* xSSE = (__m128*)x; + for (int index = 0; index < _arraySize / 4; index++) + { + *ySSE = _mm_add_ps(*ySSE, *xSSE); + xSSE++; + ySSE++; + } +} + +////////////////////////////////////////////////////////////////////// +// SSE multiply +// Y = X * Y +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::multiplySSE(float* x, float* y) +{ + __m128* ySSE = (__m128*)y; + __m128* xSSE = (__m128*)x; + for (int index = 0; index < _arraySize / 4; index++) + { + *ySSE = _mm_mul_ps(*ySSE, *xSSE); + xSSE++; + ySSE++; + } +} + +////////////////////////////////////////////////////////////////////// +// SSE multiply +// Z = X * Y +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::multiplySSE(float* x, float* y, float* z) +{ + __m128* zSSE = (__m128*)z; + __m128* ySSE = (__m128*)y; + __m128* xSSE = (__m128*)x; + for (int index = 0; index < _arraySize / 4; index++) + { + *zSSE = _mm_mul_ps(*ySSE, *xSSE); + xSSE++; + ySSE++; + zSSE++; + } +} + +////////////////////////////////////////////////////////////////////// +// SSE multiply +// Z = W - X * Y +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::multiplySubtractSSE(float* w, float* x, float* y, float* z) +{ + __m128* zSSE = (__m128*)z; + __m128* ySSE = (__m128*)y; + __m128* xSSE = (__m128*)x; + __m128* wSSE = (__m128*)w; + for (int index = 0; index < _arraySize / 4; index++) + { + *zSSE = _mm_mul_ps(*ySSE, *xSSE); + *zSSE = _mm_sub_ps(*wSSE, *zSSE); + + xSSE++; + ySSE++; + zSSE++; + wSSE++; + } +} + +////////////////////////////////////////////////////////////////////// +// SSE set +// X = val +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::setSSE(float* x, float val) +{ + __m128* xSSE = (__m128*)x; + for (int index = 0; index < _arraySize / 4; index++) + { + *xSSE = _mm_set_ps1(val); + xSSE++; + } +} + +////////////////////////////////////////////////////////////////////// +// SSE set +// X = 0 +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::wipeSSE(float* x) +{ + __m128* xSSE = (__m128*)x; + for (int index = 0; index < _arraySize / 4; index++) + { + *xSSE = _mm_setzero_ps(); + xSSE++; + } +} + +////////////////////////////////////////////////////////////////////// +// SSE set +// X = Y +////////////////////////////////////////////////////////////////////// +void CG_SOLVER_SSE::copySSE(float* x, float* y) +{ + __m128* ySSE = (__m128*)y; + for (int index = 0; index < _arraySize / 4; index++) + { + _mm_store_ps(x,*ySSE); + x += 4; + ySSE++; + } +} diff --git a/CG_SOLVER_SSE.h b/CG_SOLVER_SSE.h new file mode 100644 index 0000000..4475944 --- /dev/null +++ b/CG_SOLVER_SSE.h @@ -0,0 +1,103 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : CG_SOLVER_SSE.h +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#ifndef CG_SOLVER_SSE_H +#define CG_SOLVER_SSE_H + +#include "CG_SOLVER.h" +#include + +//////////////////////////////////////////////////////////////////// +/// \brief Conjugate gradient Poisson solver that exploits SSE. +//////////////////////////////////////////////////////////////////// +class CG_SOLVER_SSE : public CG_SOLVER +{ +public: + //! constructor + CG_SOLVER_SSE(int maxDepth, int iterations = 10, int digits = 1); + //! destructor + ~CG_SOLVER_SSE(); + + //! solve the Poisson problem using SSE + virtual int solve(list cells); + +private: + //! reallocate the SSE-friendly scratch arrays + virtual void reallocate(); + + // SSE linear algebra operators + inline float dotSSE(float* x, float* y); + inline void saxpySSE(float a, float* x, float* y); + inline void saypxSSE(float a, float* x, float* y); + inline float maxSSE(float* x); + inline void addSSE(float* x, float* y); + inline void multiplySSE(float* x, float* y); + inline void multiplySSE(float* x, float* y, float* z); + inline void multiplySubtractSSE(float* w, float* x, float* y, float* z); + inline void setSSE(float* x, float val); + inline void wipeSSE(float* x); + inline void copySSE(float* x, float* y); +}; + +#endif diff --git a/DAG.cpp b/DAG.cpp new file mode 100644 index 0000000..a2d7d28 --- /dev/null +++ b/DAG.cpp @@ -0,0 +1,566 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : DAG.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#include "DAG.h" + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +DAG::DAG(int xRes, int yRes) : + _xRes(xRes), + _yRes(yRes), + _width(xRes), + _height(yRes), + _dx(1.0f / xRes), + _dy(1.0f / yRes), + _root(NULL), + _totalNodes(0), + _bottomHit(-1), + _secondaryIntensity(0.3f), + _leaderIntensity(0.75f) +{ + (_dx < _dy) ? _dy = _dx : _dy = _dx; + _offscreenBuffer = NULL; +} + +DAG::~DAG() +{ + if (_offscreenBuffer) delete[] _offscreenBuffer; + + deleteNode(_root); +} + +////////////////////////////////////////////////////////////////////// +// delete the nodes +////////////////////////////////////////////////////////////////////// +void DAG::deleteNode(NODE* root) +{ + if (root == NULL) return; + + for (int x = 0; x < root->neighbors.size(); x++) + deleteNode(root->neighbors[x]); + + delete root; + root = NULL; +} + +////////////////////////////////////////////////////////////////////// +// add line segment 'index' to segment list +////////////////////////////////////////////////////////////////////// +bool DAG::addSegment(int index, int neighbor) +{ + if (_root) + { + // find corresponding root in DAG + map::iterator iter = _hash.find(neighbor); + NODE* root = (*iter).second; + + if (root == NULL) return false; + + // add to DAG + NODE* newNode = new NODE(index); + newNode->parent = root; + root->neighbors.push_back(newNode); + + // add to hash table + _hash.insert(map::value_type(index, newNode)); + } + else + { + // make the root + _root = new NODE(neighbor); + _hash.insert(map::value_type(neighbor, _root)); + + // then do the add + NODE* newNode = new NODE(index); + newNode->parent = _root; + _root->neighbors.push_back(newNode); + + // add to hash table + _hash.insert(map::value_type(index, newNode)); + } + + _totalNodes++; + + return true; +} + +////////////////////////////////////////////////////////////////////// +// build the leader chain +////////////////////////////////////////////////////////////////////// +void DAG::buildLeader(int bottomHit) +{ + _bottomHit = bottomHit; + + // get pointer to bottommost node + map::iterator iter = _hash.find(bottomHit); + NODE* child = (*iter).second; + + // crawl up the tree + while (child != NULL) + { + // tag segment + child->leader = true; + child->secondary = false; + + // look for side branches + for (int x = 0; x < child->neighbors.size(); x++) + if (!(child->neighbors[x]->leader)) + buildBranch(child->neighbors[x], 1); + + // advance child + child = child->parent; + } + buildIntensity(_root); +} + +////////////////////////////////////////////////////////////////////// +// build the side branch +////////////////////////////////////////////////////////////////////// +void DAG::buildBranch(NODE* node, int depth) +{ + node->depth = depth; + node->leader = false; + + // look for side branches + for (int x = 0; x < node->neighbors.size(); x++) + if (!(node->neighbors[x]->leader)) + buildBranch(node->neighbors[x], depth + 1); +} + +////////////////////////////////////////////////////////////////////// +// draw the DAG segments +////////////////////////////////////////////////////////////////////// +void DAG::drawNode(NODE* root) +{ + if (root == NULL) return; + + // draw segments + int beginIndex = root->index; + int begin[2]; + int end[2]; + float dWidth = _dx; + float dHeight = _dy; + + for (int x = 0; x < root->neighbors.size(); x++) + { + // get end node + NODE* endNode = root->neighbors[x]; + int endIndex = endNode->index; + + // draw segments + begin[1] = beginIndex / _xRes; + begin[0] = beginIndex - begin[1] * _xRes; + end[1] = endIndex / _xRes; + end[0] = endIndex - end[1] * _xRes; + + if (_bottomHit != -1) + glColor4f(endNode->intensity, + endNode->intensity, + endNode->intensity,1.0f); + else + glColor4f(0.0f, 1.0f, 0.0f, 1.0f); + + glBegin(GL_LINES); + glVertex3f(begin[0] * dWidth + dWidth * 0.5f, 1.0f - begin[1] * dHeight + dHeight * 0.5f, 0.1f); + glVertex3f(end[0] * dWidth + dWidth * 0.5f, 1.0f - end[1] * dHeight + dHeight * 0.5f, 0.1f); + glEnd(); + + // call recursively + if (endNode->neighbors.size() > 0) + drawNode(endNode); + } +} + +////////////////////////////////////////////////////////////////////// +// set the intensity of the node +////////////////////////////////////////////////////////////////////// +void DAG::buildIntensity(NODE* root) +{ + if (root == NULL) return; + + // draw segments + int beginIndex = root->index; + int begin[2]; + int end[2]; + float dWidth = _dx; + float dHeight = _dy; + + for (int x = 0; x < root->neighbors.size(); x++) + { + // get end node + NODE* endNode = root->neighbors[x]; + int endIndex = endNode->index; + + // draw segments + begin[1] = beginIndex / _xRes; + begin[0] = beginIndex - begin[1] * _xRes; + end[1] = endIndex / _xRes; + end[0] = endIndex - end[1] * _xRes; + + // set color + if (endNode->leader) + endNode->intensity = _leaderIntensity; + else + { + // find max depth of current channel + if (endNode->maxDepthNode == NULL) + findDeepest(endNode, endNode->maxDepthNode); + + int maxDepth = endNode->maxDepthNode->depth; + + // calc standard deviation + float stdDev = -(float)(maxDepth * maxDepth) / (float)(log(_secondaryIntensity) * 2.0f); + + // calc falloff + float eTerm = -(float)(endNode->depth) * (float)(endNode->depth); + eTerm /= (2.0f * stdDev); + eTerm = exp(eTerm) * 0.5f; + endNode->intensity = eTerm; + } + + // call recursively + if (endNode->neighbors.size() > 0) + buildIntensity(endNode); + } +} + +////////////////////////////////////////////////////////////////////// +// draw the tree offscreen +////////////////////////////////////////////////////////////////////// +float*& DAG::drawOffscreen(int scale) +{ + // allocate buffer + _width = _xRes * scale; + _height = _yRes * scale; + _scale = scale; + if (_offscreenBuffer) delete[] _offscreenBuffer; + _offscreenBuffer = new float[_width * _height]; + + // wipe the buffer + for (int x = 0; x < _width * _height; x++) + _offscreenBuffer[x] = 0.0f; + + // recursively draw the tree + drawOffscreenNode(_root); + + return _offscreenBuffer; +} + +////////////////////////////////////////////////////////////////////// +// draw the DAG segments +////////////////////////////////////////////////////////////////////// +void DAG::drawOffscreenNode(NODE* root) +{ + if (root == NULL) return; + + // draw segments + int beginIndex = root->index; + int begin[2]; + int end[2]; + float dWidth = _dx; + float dHeight = _dy; + + for (int x = 0; x < root->neighbors.size(); x++) + { + // get end node + NODE* endNode = root->neighbors[x]; + int endIndex = endNode->index; + + // get endpoints + begin[0] = beginIndex % _xRes * _scale; + begin[1] = beginIndex / _xRes * _scale; + end[0] = endIndex % _xRes * _scale; + end[1] = endIndex / _xRes * _scale; + + // make sure the one with the smaller x comes first + if (end[0] < begin[0]) + { + int temp[] = {end[0], end[1]}; + end[0] = begin[0]; + end[1] = begin[1]; + + begin[0] = temp[0]; + begin[1] = temp[1]; + } + + // rasterize + drawLine(begin, end, endNode->intensity); + + // call recursively + if (endNode->neighbors.size() > 0) + drawOffscreenNode(endNode); + } +} + +////////////////////////////////////////////////////////////////////// +// rasterize the line +// I assume all the lines are purely horizontal, vertical or diagonal +// to avoid using something like Bresenham +////////////////////////////////////////////////////////////////////// +void DAG::drawLine(int begin[], int end[], float intensity) +{ + int scaledX = _xRes * _scale; + + // if it is a horizontal line + if (begin[1] == end[1]) + { + for (int x = begin[0]; x < end[0]; x++) + { + int index = x + end[1] * scaledX; + if (intensity > _offscreenBuffer[index]) + _offscreenBuffer[index] = intensity; + } + return; + } + + // if it is a vertical line + if (begin[0] == end[0]) + { + int bottom = (begin[1] > end[1]) ? end[1] : begin[1]; + int top = (begin[1] > end[1]) ? begin[1] : end[1]; + + for (int y = bottom; y < top; y++) + { + int index = begin[0] + y * scaledX; + if (intensity > _offscreenBuffer[index]) + _offscreenBuffer[index] = intensity; + } + return; + } + + // else it is diagonal + int slope = (begin[1] < end[1]) ? 1 : -1; + int interval = end[0] - begin[0]; + for (int x = 0; x <= interval; x++) + { + int index = begin[0] + x + (begin[1] + x * slope) * scaledX; + if (intensity > _offscreenBuffer[index]) + _offscreenBuffer[index] = intensity; + } +} + +////////////////////////////////////////////////////////////////////// +// find deepest depth from current root +////////////////////////////////////////////////////////////////////// +void DAG::findDeepest(NODE* root, NODE*& deepest) +{ + deepest = root; + + for (int x = 0; x < root->neighbors.size(); x++) + { + NODE* child = root->neighbors[x]; + NODE* candidate = NULL; + findDeepest(child, candidate); + if (candidate->depth > deepest->depth) + deepest = candidate; + } +} + +////////////////////////////////////////////////////////////////////// +// dump out line segments +////////////////////////////////////////////////////////////////////// +void DAG::write(const char* filename) +{ + // open file + FILE* file; + file = fopen(filename, "wb"); + + // write out total number of DAG nodes + fwrite((void*)&_totalNodes, sizeof(int), 1, file); + fwrite((void*)&_xRes, sizeof(int), 1, file); + fwrite((void*)&_yRes, sizeof(int), 1, file); + fwrite((void*)&_dx, sizeof(float), 1, file); + fwrite((void*)&_dy, sizeof(float), 1, file); + fwrite((void*)&_bottomHit, sizeof(int), 1, file); + fwrite((void*)&_inputWidth, sizeof(int), 1, file); + fwrite((void*)&_inputHeight, sizeof(int), 1, file); + + // write out nodes + writeNode(_root, file); + + fclose(file); +} + +////////////////////////////////////////////////////////////////////// +// read in line segments +////////////////////////////////////////////////////////////////////// +void DAG::read(const char* filename) +{ + + // erase old DAG + deleteNode(_root); + + // open file + FILE* file; + file = fopen(filename, "rb"); + if (file == NULL) + { + cout << "ERROR: " << filename << " is invalid." << endl; + exit(1); + } + + // read in total number of DAG nodes + fread((void*)&_totalNodes, sizeof(int), 1, file); + fread((void*)&_xRes, sizeof(int), 1, file); + fread((void*)&_yRes, sizeof(int), 1, file); + fread((void*)&_dx, sizeof(float), 1, file); + fread((void*)&_dy, sizeof(float), 1, file); + fread((void*)&_bottomHit, sizeof(int), 1, file); + fread((void*)&_inputWidth, sizeof(int), 1, file); + fread((void*)&_inputHeight, sizeof(int), 1, file); + + // clear _hash for use + _hash.clear(); + + // read in all the DAG nodes + for (int x = 0; x <= _totalNodes; x++) + readNode(file); + + fclose(file); + + if (_bottomHit != -1) + buildLeader(_bottomHit); +} + +////////////////////////////////////////////////////////////////////// +// write out a DAG node +////////////////////////////////////////////////////////////////////// +void DAG::writeNode(NODE* root, FILE* file) +{ + int x; + + // write out neighbors + int numNeighbors = root->neighbors.size(); + for (x = 0; x < numNeighbors; x++) + writeNode(root->neighbors[x], file); + + // write out this node + fwrite((void*)&(root->index), sizeof(int), 1, file); + int parent = (root->parent == NULL) ? -1 : root->parent->index; + fwrite((void*)&parent, sizeof(int), 1, file); + fwrite((void*)&(root->leader), sizeof(bool), 1, file); + fwrite((void*)&(root->secondary), sizeof(bool), 1, file); + fwrite((void*)&(root->depth), sizeof(int), 1, file); + fwrite((void*)&numNeighbors, sizeof(int), 1, file); + for (x = 0; x < numNeighbors; x++) + fwrite((void*)&(root->neighbors[x]->index), sizeof(int), 1, file); +} + +////////////////////////////////////////////////////////////////////// +// read in a DAG node +////////////////////////////////////////////////////////////////////// +void DAG::readNode(FILE* file) +{ + // read in DAG data + int index; + int parent; + bool leader; + bool secondary; + int depth; + int numNeighbors; + float potential; + fread((void*)&index, sizeof(int), 1, file); + fread((void*)&parent, sizeof(int), 1, file); + fread((void*)&leader, sizeof(bool), 1, file); + fread((void*)&secondary, sizeof(bool), 1, file); + fread((void*)&depth, sizeof(int), 1, file); + fread((void*)&numNeighbors, sizeof(int), 1, file); + + // create the node + NODE* node = new NODE(index); + node->leader = leader; + node->secondary = secondary; + node->depth = depth; + + // look up neighbors + map::iterator iter; + for (int x = 0; x < numNeighbors; x++) + { + // read in the child index + int neighborIndex; + fread((void*)&neighborIndex, sizeof(int), 1, file); + + // look up in hash table + iter = _hash.find(neighborIndex); + + // push onto the neighbor vector + node->neighbors.push_back((*iter).second); + + // set child's parent + (*iter).second->parent = node; + } + + // add to hash table + _hash.insert(map::value_type(index, node)); + + // search for root node + if (parent == -1) + { + node->parent = NULL; + _root = node; + } +} diff --git a/DAG.h b/DAG.h new file mode 100644 index 0000000..9ba3a9f --- /dev/null +++ b/DAG.h @@ -0,0 +1,208 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : DAG.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#ifndef DAG_H +#define DAG_H + +#include +#include +#include +#include +#include + +using namespace std; + +//////////////////////////////////////////////////////////////////// +/// \brief Directed acyclic graph that renders the final lightning +//////////////////////////////////////////////////////////////////// +class DAG +{ +public: + //! constructor + DAG(int xRes, int yRes); + //! destructor + virtual ~DAG(); + + //! build the stepped ladder + void buildLeader(int bottomHit); + + //! add DAG segment + bool addSegment(int index, int neighbor); + + //! draw to OpenGL + void draw() { drawNode(_root); }; + + //! draw to an offscreen buffer + float*& drawOffscreen(int scale = 1); + + //! read in a new DAG + void read(const char* filename); + //! write out the current DAG + void write(const char* filename); + + //! quadtree x resolution accessor + int xRes() { return _xRes; }; + //! quadtree y resolution accessor + int yRes() { return _yRes; }; + + //! input image x resolution accessor + int& inputWidth() { return _inputWidth; }; + //! input image y resolution accessor + int& inputHeight() { return _inputHeight; }; + +private: + //! x resolution of quadtree + int _xRes; + //! y resolution of quadtree + int _yRes; + //! physical length of one grid cell + float _dx; + //! physical length of one grid cell + float _dy; + + //////////////////////////////////////////////////////////////////// + /// \brief node for line segment tree + //////////////////////////////////////////////////////////////////// + struct NODE { + int index; + vector neighbors; + NODE* parent; + bool leader; + bool secondary; + int depth; + NODE* maxDepthNode; + float intensity; + + NODE(int indexIn) { + index = indexIn; + parent = NULL; + leader = false; + depth = 0; + maxDepthNode = NULL; + }; + }; + //! recursive destructor + void deleteNode(NODE* root); + + //! root of DAG + NODE* _root; + + //! hash table of DAG nodes + map _hash; + + //! build side branch + void buildBranch(NODE* node, int depth); + //! draw a node to OpenGL + void drawNode(NODE* root); + //! find the deepest node in a given subtree + void findDeepest(NODE* root, NODE*& deepest); + + //! read in a DAG node + void readNode(FILE* file); + //! write out a DAG node + void writeNode(NODE* root, FILE* file); + + //! total number of nodes in scene + int _totalNodes; + + //! node that finally hit bottom + int _bottomHit; + + //! set the line segment intensities + void buildIntensity(NODE* root); + + //! brightness of secondary branch + float _secondaryIntensity; + //! brightness of primary branch + float _leaderIntensity; + + //////////////////////////////////////////////////////////////// + // offscreen buffer variables + //////////////////////////////////////////////////////////////// + //! offscreen buffer + float* _offscreenBuffer; + + //! width of offscreen buffer + int _width; + + //! height of offscreen buffer + int _height; + + //! scale of offscreen buffer compared to original image + int _scale; + + //! draw a given node in the DAG + void drawOffscreenNode(NODE* root); + + //! rasterize a single line to the offscreen buffer + void drawLine(int begin[], int end[], float intensity); + + //! input image x resolution + int _inputWidth; + //! input image y resolution + int _inputHeight; +}; + +#endif diff --git a/EXR.cpp b/EXR.cpp new file mode 100644 index 0000000..8cdbabd --- /dev/null +++ b/EXR.cpp @@ -0,0 +1,96 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : EXR.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#include "EXR.h" + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +EXR::EXR() +{ + +} + +EXR::~EXR() +{ + +} + +void EXR::writeEXR(const char filename[], float* image, int width, int height) +{ + Imf::Rgba* exrImage = new Imf::Rgba[width * height]; + for (int x = 0; x < width * height; x++) + { + exrImage[x].r = exrImage[x].g = exrImage[x].b = image[x]; + exrImage[x].a = 1.0f; + } + + Imf::RgbaOutputFile file (filename, width, height); + file.setFrameBuffer (exrImage, 1, width); + file.writePixels (height); + + delete[] exrImage; +} diff --git a/EXR.h b/EXR.h new file mode 100644 index 0000000..5dbdca5 --- /dev/null +++ b/EXR.h @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : EXR.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#ifndef EXR_H +#define EXR_H +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////// +/// \brief Wrapper for the ILM OpenEXR routines +//////////////////////////////////////////////////////////////////// +class EXR +{ +public: + EXR(); + virtual ~EXR(); + + /// \brief write a float array out to an EXR file + /// + /// \param filename name of output file + /// \param image float array to write to an EXR file + /// \param width width of float array + /// \param height height of float array + static void writeEXR(const char* filename, float* image, int width, int height); +}; + +#endif diff --git a/FFT.cpp b/FFT.cpp new file mode 100644 index 0000000..34bfb8a --- /dev/null +++ b/FFT.cpp @@ -0,0 +1,233 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : FFT.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#include "FFT.h" + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +FFT::FFT() +{ + +} + +FFT::~FFT() +{ + +} + +bool FFT::convolve(float* source, float* kernel, int xSource, int ySource, int xKernel, int yKernel) +{ + int x, y, index; + + // get normalization params + float maxCurrent = 0.0f; + for (x = 0; x < xSource * ySource; x++) + maxCurrent = (maxCurrent < source[x]) ? source[x] : maxCurrent; + float maxKernel = 0.0f; + for (x = 0; x < xKernel * yKernel; x++) + maxKernel = (maxKernel < kernel[x]) ? kernel[x] : maxKernel; + float maxProduct = maxCurrent * maxKernel; + + // retrieve dimensions + int xHalf = xKernel / 2; + int yHalf = yKernel / 2; + int xResPadded = xSource + xKernel; + int yResPadded = ySource + yKernel; + + if (xResPadded != yResPadded) + (xResPadded > yResPadded) ? yResPadded = xResPadded : xResPadded = yResPadded; + + // create padded field + fftw_complex* padded = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * xResPadded * yResPadded); + if (!padded) + { + cout << " IMAGE: Not enough memory! Try a smaller final image size." << endl; + return false; + } + + // init padded field + for (index = 0; index < xResPadded * yResPadded; index++) + padded[index][0] = padded[index][1] = 0.0f; + index = 0; + for (y = 0; y < ySource; y++) + for (x = 0; x < xSource; x++, index++) + { + int paddedIndex = (x + xKernel / 2) + (y + yKernel / 2) * xResPadded; + padded[paddedIndex][0] = source[index]; + } + + // create padded filter + fftw_complex* filter = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * xResPadded * yResPadded); + if (!filter) + { + cout << " FILTER: Not enough memory! Try a smaller final image size." << endl; + return false; + } + + // init padded filter + for (index = 0; index < xResPadded * yResPadded; index++) + filter[index][0] = filter[index][1] = 0.0f; + + // quadrant IV + for (y = 0; y < (yHalf + 1); y++) + for (x = 0; x < (xHalf + 1); x++) + { + int filterIndex = x + xHalf + y * xKernel; + int fieldIndex = x + (y + yResPadded - (yHalf + 1)) * xResPadded; + filter[fieldIndex][0] = kernel[filterIndex]; + } + + // quadrant I + for (y = 0; y < yHalf; y++) + for (x = 0; x < (xHalf + 1); x++) + { + int filterIndex = (x + xHalf) + (y + yHalf + 1) * xKernel; + int fieldIndex = x + y * xResPadded; + filter[fieldIndex][0] = filter[fieldIndex][1] = kernel[filterIndex]; + } + + // quadrant III + for (y = 0; y < (yHalf + 1); y++) + for (x = 0; x < xHalf; x++) + { + int filterIndex = x + y * xKernel; + int fieldIndex = (x + xResPadded - xHalf) + (y + yResPadded - (yHalf + 1)) * xResPadded; + filter[fieldIndex][0] = filter[fieldIndex][1] = kernel[filterIndex]; + } + + // quadrant II + for (y = 0; y < yHalf; y++) + for (x = 0; x < xHalf; x++) + { + int filterIndex = x + (y + yHalf + 1) * xKernel; + int fieldIndex = (x + xResPadded - xHalf) + y * xResPadded; + filter[fieldIndex][0] = filter[fieldIndex][1] = kernel[filterIndex]; + } + + // perform forward FFT on field + fftw_complex* paddedTransformed = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * xResPadded * yResPadded); + if (!paddedTransformed) + { + cout << " T-IMAGE: Not enough memory! Try a smaller final image size." << endl; + return false; + } + fftw_plan forwardField = fftw_plan_dft_2d(xResPadded, yResPadded, padded, paddedTransformed, FFTW_FORWARD, FFTW_ESTIMATE); + fftw_execute(forwardField); + + // perform forward FFT on filter + fftw_complex* filterTransformed = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * xResPadded * yResPadded); + if (!filterTransformed) + { + cout << " T-FILTER: Not enough memory! Try a smaller final image size." << endl; + return false; + } + fftw_plan forwardFilter = fftw_plan_dft_2d(xResPadded, yResPadded, filter, filterTransformed, FFTW_FORWARD, FFTW_ESTIMATE); + fftw_execute(forwardFilter); + + // apply frequency space filter + for (index = 0; index < xResPadded * yResPadded; index++) + { + float newReal = paddedTransformed[index][0] * filterTransformed[index][0] - + paddedTransformed[index][1] * filterTransformed[index][1]; + float newIm = paddedTransformed[index][0] * filterTransformed[index][1] + + paddedTransformed[index][1] * filterTransformed[index][0]; + paddedTransformed[index][0] = newReal; + paddedTransformed[index][1] = newIm; + } + + // transform back + fftw_plan backwardField = fftw_plan_dft_2d(xResPadded, yResPadded, paddedTransformed, padded, FFTW_BACKWARD, FFTW_ESTIMATE); + fftw_execute(backwardField); + + // copy back into padded + index = 0; + for (y = 0; y < ySource; y++) + for (x = 0; x < xSource; x++, index++) + { + int paddedIndex = (x + xKernel / 2) + (y + yKernel / 2) * xResPadded; + source[index] = padded[paddedIndex][0]; + } + + // clean up + fftw_free(padded); + fftw_free(paddedTransformed); + fftw_free(filter); + fftw_free(filterTransformed); + + // if normalization is exceeded, renormalize + float newMax = 0.0f; + for (x = 0; x < xSource * ySource; x++) + newMax = (newMax < source[x]) ? source[x] : newMax; + if (newMax > maxProduct) + { + float scale = maxProduct / newMax; + for (x = 0; x < xSource * ySource; x++) + source[x] *= scale; + } + + return true; +} diff --git a/FFT.h b/FFT.h new file mode 100644 index 0000000..58205f6 --- /dev/null +++ b/FFT.h @@ -0,0 +1,95 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : FFT.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#ifndef FFT_H +#define FFT_H + +#include +#include + +using namespace std; + +//////////////////////////////////////////////////////////////////// +/// \brief Wrapper class for FFTW +//////////////////////////////////////////////////////////////////// +class FFT +{ +public: + FFT(); + virtual ~FFT(); + /// \brief convolve image and filter using FFTW + /// + /// \param source source image + /// \param kernel convolution kernel + /// \param xSource width of source image + /// \param ySource height of source image + /// \param xKernel width of kernel + /// \param yKernel height yidth of kernel + /// + /// \return Returns the convolved image in the 'image' array. If the convolve fails, returns false + static bool convolve(float* source, float* kernel, int xSource, int ySource, int xKernel, int yKernel); +}; + +#endif diff --git a/LumosQuad.sln b/LumosQuad.sln new file mode 100644 index 0000000..a461672 --- /dev/null +++ b/LumosQuad.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 9.00 +# Visual C++ Express 2005 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LumosQuad", "LumosQuad.vcproj", "{5BC95B61-43C1-46C2-BA59-D95F6C336355}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5BC95B61-43C1-46C2-BA59-D95F6C336355}.Debug|Win32.ActiveCfg = Debug|Win32 + {5BC95B61-43C1-46C2-BA59-D95F6C336355}.Debug|Win32.Build.0 = Debug|Win32 + {5BC95B61-43C1-46C2-BA59-D95F6C336355}.Release|Win32.ActiveCfg = Release|Win32 + {5BC95B61-43C1-46C2-BA59-D95F6C336355}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/LumosQuad.vcproj b/LumosQuad.vcproj new file mode 100644 index 0000000..5a62773 --- /dev/null +++ b/LumosQuad.vcproj @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..de3a969 --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +all: build/lumosquad + +build/lumosquad: build/lumosquad.o build/APSF.o build/CELL.o build/CG_SOLVER_SSE.o build/CG_SOLVER.o build/DAG.o build/EXR.o build/FFT.o build/QUAD_DBM_2D.o build/QUAD_POISSON.o build/ppm.o build/BLUE_NOISE.o build/RangeList.o build/RNG.o build/ScallopedSector.o build/WeightedDiscretePDF.o + g++ -g -o build/lumosquad build/*.o -lGL -lglut -lGLU -lfftw3 -lOpenEXR -lImath + +build/lumosquad.o: main.cpp + g++ -g -O3 -o build/lumosquad.o -c main.cpp -I/usr/include/Imath + +build/APSF.o: + g++ -g -O3 -o build/APSF.o -c APSF.cpp + +build/CELL.o: + g++ -g -O3 -o build/CELL.o -c CELL.cpp + +build/CG_SOLVER_SSE.o: + g++ -g -O3 -o build/CG_SOLVER_SSE.o -c CG_SOLVER_SSE.cpp + +build/CG_SOLVER.o: + g++ -g -O3 -o build/CG_SOLVER.o -c CG_SOLVER.cpp + +build/DAG.o: + g++ -g -O3 -o build/DAG.o -c DAG.cpp + +build/EXR.o: + g++ -g -O3 -o build/EXR.o -c EXR.cpp -I/usr/include/Imath + +build/FFT.o: + g++ -g -O3 -o build/FFT.o -c FFT.cpp + +build/QUAD_DBM_2D.o: + g++ -g -O3 -o build/QUAD_DBM_2D.o -c QUAD_DBM_2D.cpp + +build/QUAD_POISSON.o: + g++ -g -O3 -o build/QUAD_POISSON.o -c QUAD_POISSON.cpp + +build/ppm.o: + g++ -g -O3 -o build/ppm.o -c ppm/ppm.cpp + +build/BLUE_NOISE.o: + g++ -g -O3 -o build/BLUE_NOISE.o -c BlueNoise/BLUE_NOISE.cpp + +build/RangeList.o: + g++ -g -O3 -o build/RangeList.o -c BlueNoise/RangeList.cpp + +build/RNG.o: + g++ -g -O3 -o build/RNG.o -c BlueNoise/RNG.cpp + +build/ScallopedSector.o: + g++ -g -O3 -o build/ScallopedSector.o -c BlueNoise/ScallopedSector.cpp + +build/WeightedDiscretePDF.o: + g++ -g -O3 -o build/WeightedDiscretePDF.o -c BlueNoise/WeightedDiscretePDF.cpp diff --git a/QUAD_DBM_2D.cpp b/QUAD_DBM_2D.cpp new file mode 100644 index 0000000..036469c --- /dev/null +++ b/QUAD_DBM_2D.cpp @@ -0,0 +1,461 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : QUAD_DBM_2D.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#include "QUAD_DBM_2D.h" + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +QUAD_DBM_2D::QUAD_DBM_2D(int xRes, int yRes, int iterations) : + _xRes(xRes), + _yRes(yRes), + _bottomHit(0), + _iterations(iterations), + _quadPoisson(NULL), + _dag(NULL), + _skips(10), + _twister(123456) +{ + allocate(); + _dag = new DAG(_xRes, _yRes); + + // calculate dimensions + _dx = 1.0f / (float)_xRes; + _dy = 1.0f / (float)_yRes; + if (_dx < _dy) _dy = _dx; + else _dx = _dy; + + _maxRes = _xRes * _yRes; +} + +QUAD_DBM_2D::~QUAD_DBM_2D() +{ + deallocate(); +} + +void QUAD_DBM_2D::allocate() +{ + _quadPoisson = new QUAD_POISSON(_xRes, _yRes, _iterations); + _xRes = _yRes = _quadPoisson->maxRes(); +} + +void QUAD_DBM_2D::deallocate() +{ + if (_dag) delete _dag; + if (_quadPoisson) delete _quadPoisson; +} + +////////////////////////////////////////////////////////////////////// +// check neighbors for any candidate nodes +////////////////////////////////////////////////////////////////////// +void QUAD_DBM_2D::checkForCandidates(CELL* cell) +{ + int maxDepth = _quadPoisson->maxDepth(); + + CELL* north = cell->northNeighbor(); + if (north) { + if (north->depth == maxDepth) { + if (!north->candidate) { + _candidates.push_back(north); + north->candidate = true; + } + + CELL* northeast = north->eastNeighbor(); + if (northeast && !northeast->candidate) { + _candidates.push_back(northeast); + northeast->candidate = true; + } + CELL* northwest = north->westNeighbor(); + if (northwest && !northwest->candidate) { + _candidates.push_back(northwest); + northwest->candidate = true; + } + } + } + + CELL* east = cell->eastNeighbor(); + if (east && !east->candidate) { + _candidates.push_back(east); + east->candidate = true; + } + + CELL* south = cell->southNeighbor(); + if (south) { + if (!south->candidate) { + _candidates.push_back(south); + south->candidate = true; + } + + CELL* southeast = south->eastNeighbor(); + if (southeast && !southeast->candidate) { + _candidates.push_back(southeast); + southeast->candidate = true; + } + + CELL* southwest = south->westNeighbor(); + if (southwest && !southwest->candidate) { + _candidates.push_back(southwest); + southwest->candidate = true; + } + } + + CELL* west = cell->westNeighbor(); + if (west && !west->candidate) { + _candidates.push_back(west); + west->candidate = true; + } +} + +////////////////////////////////////////////////////////////////////// +// add particle to the aggregate +////////////////////////////////////////////////////////////////////// +bool QUAD_DBM_2D::addParticle() +{ + static float invSqrtTwo = 1.0f / sqrt(2.0f); + static int totalParticles = 0; + static int skipSolve = 0; + + // compute the potential + int iterations = 0; + if (!skipSolve) + iterations = _quadPoisson->solve(); + skipSolve++; + if (skipSolve == _skips) skipSolve = 0; + + // construct probability distribution + vector probabilities; + float totalPotential = 0.0f; + for (int x = 0; x < _candidates.size(); x++) + { + if (_candidates[x]->candidate) + { + probabilities.push_back(_candidates[x]->potential); + totalPotential += _candidates[x]->potential; + } + else + probabilities.push_back(0.0f); + } + + // get all the candidates + // if none are left, stop + if (_candidates.size() == 0) { + return false; + } + + // if there is not enough potential, go Brownian + int toAddIndex = 0; + if (totalPotential < 1e-8) + toAddIndex = _candidates.size() * _twister.getDoubleLR(); + // else follow DBM algorithm + else + { + // add a neighbor + float random = _twister.getDoubleLR(); + float invTotalPotential = 1.0f / totalPotential; + float potentialSeen = probabilities[0] * invTotalPotential; + while ((potentialSeen < random) && (toAddIndex < _candidates.size())) + { + toAddIndex++; + potentialSeen += probabilities[toAddIndex] * invTotalPotential; + } + } + _candidates[toAddIndex]->boundary = true; + _candidates[toAddIndex]->potential = 0.0f; + _candidates[toAddIndex]->state = NEGATIVE; + + CELL* neighbor = NULL; + CELL* added = _candidates[toAddIndex]; + CELL* north = added->northNeighbor(); + if (north) + { + if (north->state == NEGATIVE) + neighbor = north; + CELL* northeast = north->eastNeighbor(); + if (northeast && northeast->state == NEGATIVE) + neighbor = northeast; + CELL* northwest = north->westNeighbor(); + if (northwest && northwest->state == NEGATIVE) + neighbor = northwest; + } + CELL* east = added->eastNeighbor(); + if (east && east->state == NEGATIVE) + neighbor = east; + + CELL* south = added->southNeighbor(); + if (south) + { + if (south->state == NEGATIVE) + neighbor = south; + CELL* southeast = south->eastNeighbor(); + if (southeast && southeast->state == NEGATIVE) + neighbor = southeast; + CELL* southwest = south->westNeighbor(); + if (southwest && southwest->state == NEGATIVE) + neighbor = southwest; + } + CELL* west = added->westNeighbor(); + if (west && west->state == NEGATIVE) + neighbor = west; + + // insert it as a node for bookkeeping + _quadPoisson->insert(added->center[0], added->center[1]); + checkForCandidates(added); + + // insert into the dag + int newIndex = (int)(added->center[0] * _xRes) + + (int)(added->center[1] * _yRes) * _xRes; + int neighborIndex = (int)(neighbor->center[0] * _xRes) + + (int)(neighbor->center[1] * _yRes) * _xRes; + _dag->addSegment(newIndex, neighborIndex); + + totalParticles++; + if (!(totalParticles % 200)) + cout << " " << totalParticles; + + hitGround(added); + + return true; +} + +////////////////////////////////////////////////////////////////////// +// hit ground yet? +////////////////////////////////////////////////////////////////////// +bool QUAD_DBM_2D::hitGround(CELL* cell) +{ + if (_bottomHit) + return true; + + if (!cell) + return false; + + bool hit = false; + if (cell->northNeighbor()) + { + CELL* north = cell->northNeighbor(); + if (north->state == POSITIVE) + hit = true; + if (north->eastNeighbor()->state == POSITIVE) + hit = true; + if (north->westNeighbor()->state == POSITIVE) + hit = true; + } + if (cell->eastNeighbor()) + if (cell->eastNeighbor()->state == POSITIVE) + hit = true; + if (cell->southNeighbor()) + { + CELL* south = cell->southNeighbor(); + if (south->state == POSITIVE) + hit = true; + if (south->eastNeighbor()->state == POSITIVE) + hit = true; + if (south->westNeighbor()->state == POSITIVE) + hit = true; + } + if (cell->westNeighbor()) + if (cell->westNeighbor()->state == POSITIVE) + hit = true; + + if (hit) + { + _bottomHit = (int)(cell->center[0] * _xRes) + + (int)(cell->center[1] * _yRes) * _xRes; + _dag->buildLeader(_bottomHit); + return true; + } + return false; +} + +//////////////////////////////////////////////////////////////////// +// drawing functions +//////////////////////////////////////////////////////////////////// +void QUAD_DBM_2D::draw() { + glPushMatrix(); + glTranslatef(-0.5, -0.5, 0); + + list leaves; + list::iterator cellIterator = leaves.begin(); + _quadPoisson->getAllLeaves(leaves); + for (cellIterator = leaves.begin(); cellIterator != leaves.end(); cellIterator++) + { + float color = (*cellIterator)->potential; + + if ((*cellIterator)->boundary) { + if (color <= 0.0f) + _quadPoisson->drawCell(*cellIterator, 0,0,0); + else + _quadPoisson->drawCell(*cellIterator, 0,0,color); + } + else + _quadPoisson->drawCell(*cellIterator, color, 0,0); + } + _quadPoisson->draw(NULL); + + glPopMatrix(); +} + +//////////////////////////////////////////////////////////////////// +// read in attractors from an image +//////////////////////////////////////////////////////////////////// +bool QUAD_DBM_2D::readImage(unsigned char* initial, + unsigned char* attractors, + unsigned char* repulsors, + unsigned char* terminators, + int xRes, int yRes) +{ + _dag->inputWidth() = xRes; + _dag->inputHeight() = yRes; + + bool initialFound = false; + bool terminateFound = false; + + int index = 0; + for (int y = 0; y < yRes; y++) + for (int x = 0; x < xRes; x++, index++) + { + // insert initial condition + if (initial[index]) + { + // insert something + CELL* negative = _quadPoisson->insert(x, y); + negative->boundary = true; + negative->potential = 0.0f; + negative->state = NEGATIVE; + negative->candidate = true; + + checkForCandidates(negative); + + initialFound = true; + } + + // insert attractors + if (attractors[index]) + { + // insert something + CELL* positive = _quadPoisson->insert(x, y); + positive->boundary = true; + positive->potential = 1.0f; + positive->state = ATTRACTOR; + positive->candidate = true; + } + + // insert repulsors + if (repulsors[index]) + { + // only insert the repulsor if it is the edge of a repulsor + bool edge = false; + + if (x != 0) + { + if (!repulsors[index - 1]) edge = true; + if (y != 0 && !repulsors[index - xRes - 1]) edge = true; + if (y != yRes - 1 && !repulsors[index + xRes - 1]) edge = true; + } + if (x != _xRes - 1) + { + if (!repulsors[index + 1]) edge = true; + if (y != 0 && !repulsors[index - xRes + 1]) edge = true; + if (y != yRes - 1 && !repulsors[index + xRes + 1]) edge = true; + } + if (y != 0 && !repulsors[index - xRes]) edge = true; + if (y != yRes - 1 && !repulsors[index + xRes]) edge = true; + + if (edge) + { + // insert something + CELL* negative = _quadPoisson->insert(x, y); + negative->boundary = true; + negative->potential = 0.0f; + negative->state = REPULSOR; + negative->candidate = true; + } + } + + // insert terminators + if (terminators[index]) + { + // insert something + CELL* positive = _quadPoisson->insert(x, y); + positive->boundary = true; + positive->potential = 1.0f; + positive->state = POSITIVE; + positive->candidate = true; + + terminateFound = true; + } + } + + if (!initialFound) { + cout << " The lightning does not start anywhere! " << endl; + return false; + } + if (!terminateFound) { + cout << " The lightning does not end anywhere! " << endl; + return false; + } + + return true; +} diff --git a/QUAD_DBM_2D.h b/QUAD_DBM_2D.h new file mode 100644 index 0000000..bdeba3f --- /dev/null +++ b/QUAD_DBM_2D.h @@ -0,0 +1,196 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : QUAD_DBM_2D.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#ifndef QUAD_DBM_2D_H +#define QUAD_DBM_2D_H + +#include +#include +#include "DAG.h" +#include "QUAD_POISSON.h" + +//////////////////////////////////////////////////////////////////// +/// \brief Quadtree DBM solver. This is the highest level class. +//////////////////////////////////////////////////////////////////// +class QUAD_DBM_2D +{ +public: + /// \brief DBM constructor + /// + /// \param xRes maximum x resolution + /// \param yRes maximum y resolution + /// \param iterations maximum conjugate gradient iterations + QUAD_DBM_2D(int xRes = 128, int yRes = 128, int iterations = 10); + + //! destructor + virtual ~QUAD_DBM_2D(); + + //! add to aggregate + bool addParticle(); + + /// \brief Hit ground yet? + /// \return returns true if a terminator as already been hit + bool hitGround(CELL* cell = NULL); + + //! draw the quadtree cells to OpenGL + void draw(); + + //! draw the DAG to OpenGL + void drawSegments() { + glLineWidth(1.0f); + glPushMatrix(); + glTranslatef(-0.5f, -0.5f, 0.0f); + _dag->draw(); + glPopMatrix(); + }; + + //////////////////////////////////////////////////////////////// + // file IO + //////////////////////////////////////////////////////////////// + + //! write everything to a file + void writeFields(const char* filename); + + //! read everything from a file + void readFields(const char* filename); + + /// \brief read in control parameters from an input file + /// + /// \param initial initial pixels of lightning + /// \param attractors pixels that attract the lightning + /// \param repulsors pixels that repulse the lightning + /// \param terminators pixels that halt the simulation if hit + /// \param xRes x resolution of the image + /// \param yRes y resolution of the image + /// + /// \return Returns false if it finds something wrong with the images + bool readImage(unsigned char* initial, + unsigned char* attractors, + unsigned char* repulsors, + unsigned char* terminators, + int xRes, int yRes); + + //! read in a new DAG + void readDAG(const char* filename) { _dag->read(filename); }; + + //! write out the current DAG + void writeDAG(const char* filename) { _dag->write(filename); }; + + /// \brief render to a software-only buffer + /// + /// \param scale a (scale * xRes) x (scale * yRes) image is rendered + float*& renderOffscreen(int scale = 1) { return _dag->drawOffscreen(scale); }; + + //! access the DBM x resolution + int xRes() { return _xRes; }; + //! access the DBM y resolution + int yRes() { return _yRes; }; + //! access the DAG x resolution + int xDagRes() { return _dag->xRes(); }; + //! access the DAG y resolution + int yDagRes() { return _dag->yRes(); }; + //! access the x resolution of the input image + int inputWidth() { return _dag->inputWidth(); }; + //! access the y resolution of the input image + int inputHeight() { return _dag->inputHeight(); }; + +private: + void allocate(); + void deallocate(); + + //////////////////////////////////////////////////////////////////// + // dielectric breakdown model components + //////////////////////////////////////////////////////////////////// + + // field dimensions + int _xRes; + int _yRes; + int _maxRes; + float _dx; + float _dy; + int _iterations; + + // which cell did it hit bottom with? + int _bottomHit; + + DAG* _dag; + + QUAD_POISSON* _quadPoisson; + + // current candidate list + vector _candidates; + + // check if any of the neighbors of cell should be added to the + // candidate list + void checkForCandidates(CELL* cell); + + // number of particles to add before doing another Poisson solve + int _skips; + + // Mersenne Twister + RNG _twister; +}; + +#endif diff --git a/QUAD_POISSON.cpp b/QUAD_POISSON.cpp new file mode 100644 index 0000000..d8d12b5 --- /dev/null +++ b/QUAD_POISSON.cpp @@ -0,0 +1,620 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : QUAD_POISSON.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#include "QUAD_POISSON.h" + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +QUAD_POISSON::QUAD_POISSON(int xRes, int yRes, int iterations) : + _root(new CELL(1.0f, 1.0f, 0.0f, 0.0f)), + _noise() +{ + _root->refine(); + + // figure out the max depth needed + float xMax = log((float)xRes) / log(2.0f); + float yMax = log((float)yRes) / log(2.0f); + + float max = (xMax > yMax) ? xMax : yMax; + if (max - floor(max) > 1e-7) + max = max + 1; + max = floor(max); + + _maxRes = pow(2.0f, (float)max); + _maxDepth = max; + + // create the blue noise + _noiseFunc = new BLUE_NOISE(5.0f / (float)_maxRes); + _noise = new bool[_maxRes * _maxRes]; + _noiseFunc->complete(); + _noiseFunc->maximize(); + _noiseFunc->writeToBool(_noise, _maxRes); + + _solver = new CG_SOLVER(_maxDepth, iterations); +} + +QUAD_POISSON::~QUAD_POISSON() +{ + deleteGhosts(); + delete _root; + delete _solver; + delete _noiseFunc; + delete[] _noise; +} + +////////////////////////////////////////////////////////////////////// +// draw boundaries to OGL +////////////////////////////////////////////////////////////////////// +void QUAD_POISSON::draw(CELL* cell) +{ + // see if it's the root + if (cell == NULL) { + draw(_root); + return; + } + + // draw the current cell + glColor4f(1,1,1,0.1); + + glBegin(GL_LINE_STRIP); + glVertex2f(cell->bounds[1], 1.0f - cell->bounds[0]); + glVertex2f(cell->bounds[1], 1.0f - cell->bounds[2]); + glVertex2f(cell->bounds[3], 1.0f - cell->bounds[2]); + glVertex2f(cell->bounds[3], 1.0f - cell->bounds[0]); + glVertex2f(cell->bounds[1], 1.0f - cell->bounds[0]); + glEnd(); + + // draw the children + for (int x = 0; x < 4; x++) + if (cell->children[x] != NULL) + draw(cell->children[x]); +} + +////////////////////////////////////////////////////////////////////// +// draw the given cell +////////////////////////////////////////////////////////////////////// +void QUAD_POISSON::drawCell(CELL* cell, float r, float g, float b) +{ + // draw the current cell + glColor4f(r,g,b,1.0f); + glBegin(GL_QUADS); + glVertex2f(cell->bounds[1], 1.0f - cell->bounds[0]); + glVertex2f(cell->bounds[1], 1.0f - cell->bounds[2]); + glVertex2f(cell->bounds[3], 1.0f - cell->bounds[2]); + glVertex2f(cell->bounds[3], 1.0f - cell->bounds[0]); + glEnd(); +} + +////////////////////////////////////////////////////////////////////// +// subdivide quadtree to max level for (xPos, yPos) +// returns a pointer to the cell that was created +////////////////////////////////////////////////////////////////////// +CELL* QUAD_POISSON::insert(float xPos, float yPos) +{ + int currentDepth = 0; + CELL* currentCell = _root; + bool existed = true; + + while (currentDepth < _maxDepth) { + // find quadrant of current point + float diff[2]; + diff[0] = xPos - currentCell->center[0]; + diff[1] = yPos - currentCell->center[1]; + int quadrant = 1; + if (diff[0] > 0.0f) { + if (diff[1] < 0.0f) + quadrant = 2; + } + else if (diff[1] < 0.0f) + quadrant = 3; + else + quadrant = 0; + + // check if it exists + if (currentCell->children[quadrant] == NULL) { + existed = false; + currentCell->refine(); + } + + // recurse to next level + currentCell = currentCell->children[quadrant]; + + // increment depth + currentDepth++; + } + // if we had to subdivide to get the cell, add them to the list + if (!existed) + for (int i = 0; i < 4; i++) + { + _smallestLeaves.push_back(currentCell->parent->children[i]); + setNoise(currentCell->parent->children[i]); + } + + /////////////////////////////////////////////////////////////////// + // force orthogonal neighbors to be same depth + // I have commented the first block, the rest follow the same flow + /////////////////////////////////////////////////////////////////// + + // see if neighbor exists + CELL* north = currentCell->northNeighbor(); + if (north && north->depth != _maxDepth) { + // while the neighbor needs to be refined + while (north->depth != _maxDepth) { + + // refine it + north->refine(); + + // set to the newly refined neighbor + north = currentCell->northNeighbor(); + } + // add newly created nodes to the list + for (int i = 0; i < 4; i++) + { + _smallestLeaves.push_back(north->parent->children[i]); + setNoise(north->parent->children[i]); + } + } + CELL* south = currentCell->southNeighbor(); + if (south && south->depth != _maxDepth) { + while (south->depth != _maxDepth) { + south->refine(); + south = currentCell->southNeighbor(); + } + for (int i = 0; i < 4; i++) + { + _smallestLeaves.push_back(south->parent->children[i]); + setNoise(south->parent->children[i]); + } + } + CELL* west = currentCell->westNeighbor(); + if (west && west->depth != _maxDepth) { + while (west->depth != _maxDepth) { + west->refine(); + west = currentCell->westNeighbor(); + } + for (int i = 0; i < 4; i++) + { + _smallestLeaves.push_back(west->parent->children[i]); + setNoise(west->parent->children[i]); + } + } + CELL* east = currentCell->eastNeighbor(); + if (east && east->depth != _maxDepth) { + while (east->depth != _maxDepth) { + east->refine(); + east = currentCell->eastNeighbor(); + } + for (int i = 0; i < 4; i++) + { + _smallestLeaves.push_back(east->parent->children[i]); + setNoise(east->parent->children[i]); + } + } + + /////////////////////////////////////////////////////////////////// + // force diagonal neighbors to be same depth + // The same flow follows as above, except that it makes sure that + // the 'north' and 'south' neighbors already exist + /////////////////////////////////////////////////////////////////// + + if (north) { + CELL* northwest = north->westNeighbor(); + if (northwest && northwest->depth != _maxDepth) { + while (northwest->depth != _maxDepth) { + northwest->refine(); + northwest = northwest->children[2]; + } + for (int i = 0; i < 4; i++) + { + _smallestLeaves.push_back(northwest->parent->children[i]); + setNoise(northwest->parent->children[i]); + } + } + CELL* northeast = north->eastNeighbor(); + if (northeast && northeast->depth != _maxDepth) { + while (northeast->depth != _maxDepth) { + northeast->refine(); + northeast= northeast->children[3]; + } + for (int i = 0; i < 4; i++) + { + _smallestLeaves.push_back(northeast->parent->children[i]); + setNoise(northeast->parent->children[i]); + } + } + } + if (south) { + CELL* southwest = south->westNeighbor(); + if (southwest && southwest->depth != _maxDepth) { + while (southwest->depth != _maxDepth) { + southwest->refine(); + southwest = southwest->children[1]; + } + for (int i = 0; i < 4; i++) + { + _smallestLeaves.push_back(southwest->parent->children[i]); + setNoise(southwest->parent->children[i]); + } + } + CELL* southeast = south->eastNeighbor(); + if (southeast && southeast->depth != _maxDepth) { + while (southeast->depth != _maxDepth) { + southeast->refine(); + southeast= southeast->children[0]; + } + for (int i = 0; i < 4; i++) + { + _smallestLeaves.push_back(southeast->parent->children[i]); + setNoise(southeast->parent->children[i]); + } + } + } + + return currentCell; +} + +////////////////////////////////////////////////////////////////////// +// check if a cell hits a noise node +////////////////////////////////////////////////////////////////////// +void QUAD_POISSON::setNoise(CELL* cell) +{ + if (!(cell->state == EMPTY)) + return; + + int x = cell->center[0] * _maxRes; + int y = cell->center[1] * _maxRes; + + if (_noise[x + y * _maxRes]) + { + cell->boundary = true; + cell->state = ATTRACTOR; + cell->potential = 0.5f; + cell->candidate = true; + } +} + +////////////////////////////////////////////////////////////////////// +// insert all leaves into a list +////////////////////////////////////////////////////////////////////// +void QUAD_POISSON::getAllLeaves(list& leaves, CELL* currentCell) +{ + // if we're at the root + if (currentCell == NULL) + { + getAllLeaves(leaves, _root); + return; + } + + // if we're at a leaf, add it to the list + if (currentCell->children[0] == NULL) + { + leaves.push_back(currentCell); + return; + } + + // if children exist, call recursively + for (int x = 0; x < 4; x++) + getAllLeaves(leaves, currentCell->children[x]); +} + +////////////////////////////////////////////////////////////////////// +// insert all leaves not on the boundary into a list +////////////////////////////////////////////////////////////////////// +void QUAD_POISSON::getEmptyLeaves(list& leaves, CELL* currentCell) +{ + // if we're at the root + if (currentCell == NULL) { + getEmptyLeaves(leaves, _root); + return; + } + + // if we're at a leaf, check if it's a boundary and then + // add it to the list + if (currentCell->children[0] == NULL) { + if (!(currentCell->boundary)) + leaves.push_back(currentCell); + return; + } + + // if children exist, call recursively + for (int x = 0; x < 4; x++) + getEmptyLeaves(leaves, currentCell->children[x]); +} + +////////////////////////////////////////////////////////////////////// +// balance the current tree +////////////////////////////////////////////////////////////////////// +void QUAD_POISSON::balance() +{ + // collect all the leaf nodes + list leaves; + getAllLeaves(leaves); + + // while the list is not empty + list::iterator cellIterator = leaves.begin(); + for (cellIterator = leaves.begin(); cellIterator != leaves.end(); cellIterator++) { + CELL* currentCell = *cellIterator; + + // if a north neighbor exists + CELL* north = currentCell->northNeighbor(); + if (north != NULL) + // while the neighbor is not balanced + while (north->depth < currentCell->depth - 1) { + // refine it + north->refine(); + + // add the newly refined nodes to the list of + // those to be checked + for (int x = 0; x < 4; x++) + leaves.push_back(north->children[x]); + + // set the cell to the newly created one + north = currentCell->northNeighbor(); + } + + // the rest of the blocks flow the same as above + CELL* south = currentCell->southNeighbor(); + if (south!= NULL) + while (south->depth < currentCell->depth - 1) { + south->refine(); + for (int x = 0; x < 4; x++) + leaves.push_back(south->children[x]); + south = currentCell->southNeighbor(); + } + + CELL* west = currentCell->westNeighbor(); + if (west != NULL) + while (west->depth < currentCell->depth - 1) { + west->refine(); + for (int x = 0; x < 4; x++) + leaves.push_back(west->children[x]); + west = currentCell->westNeighbor(); + } + + CELL* east = currentCell->eastNeighbor(); + if (east != NULL) + while (east->depth < currentCell->depth - 1) { + east->refine(); + for (int x = 0; x < 4; x++) + leaves.push_back(east->children[x]); + east = currentCell->eastNeighbor(); + } + } +} + +////////////////////////////////////////////////////////////////////// +// build the neighbor lists of the current quadtree +////////////////////////////////////////////////////////////////////// +void QUAD_POISSON::buildNeighbors() +{ + balance(); + + // collect all the leaf nodes + list leaves; + getAllLeaves(leaves); + + list::iterator cellIterator = leaves.begin(); + for (cellIterator = leaves.begin(); cellIterator != leaves.end(); cellIterator++) + { + CELL* currentCell = *cellIterator; + + // build north neighbors + CELL* north = currentCell->northNeighbor(); + if (north != NULL) { + if (north->children[0] == NULL) { + currentCell->neighbors[0] = north; + currentCell->neighbors[1] = NULL; + } + else { + currentCell->neighbors[0] = north->children[3]; + currentCell->neighbors[1] = north->children[2]; + } + } + // else build a ghost cell + else + currentCell->neighbors[0] = new CELL(currentCell->depth); + + // build east neighbors + CELL* east = currentCell->eastNeighbor(); + if (east != NULL) { + if (east->children[0] == NULL) { + currentCell->neighbors[2] = east; + currentCell->neighbors[3] = NULL; + } + else { + currentCell->neighbors[2] = east->children[0]; + currentCell->neighbors[3] = east->children[3]; + } + } + // else build a ghost cell + else + currentCell->neighbors[2] = new CELL(currentCell->depth); + + // build south neighbors + CELL* south = currentCell->southNeighbor(); + if (south != NULL) { + if (south->children[0] == NULL) { + currentCell->neighbors[4] = south; + currentCell->neighbors[5] = NULL; + } + else { + currentCell->neighbors[4] = south->children[1]; + currentCell->neighbors[5] = south->children[0]; + } + } + // else build a ghost cell + else + currentCell->neighbors[4] = new CELL(currentCell->depth); + + // build west neighbors + CELL* west = currentCell->westNeighbor(); + if (west != NULL) { + if (west->children[0] == NULL) { + currentCell->neighbors[6] = west; + currentCell->neighbors[7] = NULL; + } + else { + currentCell->neighbors[6] = west->children[2]; + currentCell->neighbors[7] = west->children[1]; + } + } + // else build a ghost cell + else + currentCell->neighbors[6] = new CELL(currentCell->depth); + } +} + +////////////////////////////////////////////////////////////////////// +// delete ghost cells +////////////////////////////////////////////////////////////////////// +void QUAD_POISSON::deleteGhosts(CELL* currentCell) +{ + // if at the base, call on the root + if (currentCell == NULL) + { + deleteGhosts(_root); + return; + } + + // if there are children, delete those too + if (currentCell->children[0]) { + // call recursively + for (int x = 0; x < 4; x++) + deleteGhosts(currentCell->children[x]); + return; + } + + // check the neighbors for stuff to delete + for (int x = 0; x < 8; x++) { + // if the neighbor exists + if (currentCell->neighbors[x]) + // and if it is a ghost cell, delete it + if (currentCell->neighbors[x]->parent == NULL) + delete currentCell->neighbors[x]; + } +} + +////////////////////////////////////////////////////////////////////// +// solve the Poisson problem +////////////////////////////////////////////////////////////////////// +int QUAD_POISSON::solve() { + // maintain the quadtree + balance(); + buildNeighbors(); + + // retrieve leaves at the lowest level + _emptyLeaves.clear(); + getEmptyLeaves(_emptyLeaves); + + static bool firstSolve = true; + static int iterations = _solver->iterations(); + + // do a full precision solve the first time + if (firstSolve) + { + iterations = _solver->iterations(); + _solver->iterations() = 10000; + firstSolve = false; + } + else + _solver->iterations() = iterations; + + // return the number of iterations + return _solver->solve(_emptyLeaves); +}; + + +////////////////////////////////////////////////////////////////////// +// get the leafnode that corresponds to the coordinate +////////////////////////////////////////////////////////////////////// +CELL* QUAD_POISSON::getLeaf(float xPos, float yPos) +{ + CELL* currentCell = _root; + + while (currentCell->children[0] != NULL) + { + // find quadrant of current point + float diff[2]; + diff[0] = xPos - currentCell->center[0]; + diff[1] = yPos - currentCell->center[1]; + int quadrant = 1; + if (diff[0] > 0.0f) + { + if (diff[1] < 0.0f) + quadrant = 2; + } + else if (diff[1] < 0.0f) + quadrant = 3; + else + quadrant = 0; + + // check if it exists + if (currentCell->children[quadrant] != NULL) + currentCell = currentCell->children[quadrant]; + } + return currentCell; +} diff --git a/QUAD_POISSON.h b/QUAD_POISSON.h new file mode 100644 index 0000000..6d129ab --- /dev/null +++ b/QUAD_POISSON.h @@ -0,0 +1,190 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : QUAD_POISSON.h +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// + +#ifndef QUAD_POISSON_H +#define QUAD_POISSON_H + +#include +#include +#include "CELL.h" +#include +#include "CG_SOLVER.h" +#include "CG_SOLVER_SSE.h" +#include "BlueNoise/BLUE_NOISE.h" + +#include + +using namespace std; + +////////////////////////////////////////////////////////////////////// +/// \brief Quadtree Poisson solver +////////////////////////////////////////////////////////////////////// +class QUAD_POISSON +{ +public: + /// \brief quadtree constructor + /// + /// \param xRes maximum x resolution + /// \param yRes maximum y resolution + /// \param iterations maximum conjugate gradient iterations + QUAD_POISSON(int xRes, + int yRes, + int iterations = 10); + + //! destructor + virtual ~QUAD_POISSON(); + + /// \brief OpenGL drawing function + /// + /// \param cell internally used param, should always be NULL externally + void draw(CELL* cell = NULL); + + /// \brief Draw a single OpenGL cell + /// + /// \param cell quadtree cell to draw + /// \param r red intensity to draw + /// \param g green intensity to draw + /// \param b blue intensity to draw + void drawCell(CELL* cell, + float r = 1.0f, + float g = 0.0f, + float b = 0.0f); + + //! Solve the Poisson problem + int solve(); + + /// \brief insert point at maximum subdivision level + /// + /// \param xPos x position to insert at + /// \param yPos y position to insert at + CELL* insert(float xPos, float yPos); + + /// \brief insert point at maximum subdivision level + /// + /// \param xPos grid cell x index to insert + /// \param yPos grid cell y index to insert + CELL* insert(int xPos, int yPos) { + return insert((float)xPos / _maxRes, (float)yPos / _maxRes); + }; + + /// \brief get all the leaf nodes + /// \return Leaves are returned in the 'leaves' param + void getAllLeaves(list& leaves, CELL* currentCell = NULL); + + /// \brief get all the leaf nodes at finest subdivision level + /// \return Leaves are returned in the 'leaves' param + list& getSmallestLeaves() { return _smallestLeaves; }; + + //! maximum resolution accessor + int& maxRes() { return _maxRes; } + + //! maximum depth accessor + int& maxDepth() { return _maxDepth; }; + + //! get leaf at coordinate (x,y) + CELL* getLeaf(float xPos, float yPos); + +private: + //! root of the quadtree + CELL* _root; + + //! maximum resolution of quadtree + int _maxRes; + + //! maxmimum depth of quadtree + int _maxDepth; + + //! dependant leaves + list _emptyLeaves; + + //! smallest leaves + list _smallestLeaves; + + //! current Poisson solver + CG_SOLVER* _solver; + + //! balance quadtree + void balance(); + + //! get the leaf nodes not on the boundary + void getEmptyLeaves(list& leaves, CELL* currentCell = NULL); + + //! build the neighbor lists of the cells + void buildNeighbors(); + + //! delete ghost cells + void deleteGhosts(CELL* currentCell = NULL); + + //! Blue noise function + BLUE_NOISE* _noiseFunc; + + //! Blue noise sample locations + bool* _noise; + + //! check if a cell hits a noise node + void setNoise(CELL* cell); +}; + +#endif diff --git a/README.md b/README.md new file mode 100644 index 0000000..240e0a3 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# LumosQuad + +[Original code source](https://gamma.cs.unc.edu/FAST_LIGHTNING/lumos.html) slightly modified to work on Debian. + +Packages to install: `freeglut3-dev libfftw3-dev libopenexr-dev libopengl-dev` + + make + ./build/lumosquad examples/spine.ppm o.exr + +EXR files are images that can be opened by Gimp. diff --git a/examples/hint.ppm b/examples/hint.ppm new file mode 100644 index 0000000000000000000000000000000000000000..af55298b9fd0939c39b2f090034fb816729583ce GIT binary patch literal 196668 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 literal 0 HcmV?d00001 diff --git a/examples/nopath.ppm b/examples/nopath.ppm new file mode 100644 index 0000000000000000000000000000000000000000..920df521268d4ee2902fe1e242ee805b3239552b GIT binary patch literal 196668 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 literal 0 HcmV?d00001 diff --git a/examples/spine.ppm b/examples/spine.ppm new file mode 100644 index 0000000000000000000000000000000000000000..1c1b749cc7377d1ee22914e7037fd6596c3c2b6e GIT binary patch literal 196623 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 AEdT%j literal 0 HcmV?d00001 diff --git a/examples/spineless.ppm b/examples/spineless.ppm new file mode 100644 index 0000000000000000000000000000000000000000..7871fac8fe1f5d377fe342fe2fc69e059ddfb4a0 GIT binary patch literal 196668 zcmeI5f5@F>8OOg``@y@&SvV9dP8z--G-o)iW?^nR`eDwoMQhc-G2L1k?aeR>Hmq4$ zhS9P=j0Wxp2nr=dtu01W3zU|Fg<+yKFrr3gn(mjb@6HQDJje5%w{~~k+}G#fjBU?3 zJkPm4_vg9J^*qo0-1nkON6zoeUwqYjmt4E}U7aPXS9KP?@0vxUW1U4Gx~6mWnooUp z)w<4ySFIac^XZ#A7tfwE^7grxcKpYG&mGZm@qz#ZAOHafKmY;|fB*y_009U<00Izz z00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fWUuGV7GL=w2~JDPBnp>r01*e z?fl6ThYkw_PCbG1q{DUl|DyDcQy({5gun>`%j%ZjdH-+H8MGAy$`Dvzz5hF<)5^ro zRw1ypdjIdBkswfpz-y$3tM`9tnb_GX1kRHFRk#0#qzh;#2$Ui4PU%?P{y!>BD-%0g zg}}$_mfshDw$n%uC`RC`)%$--vDn!#1Rki{|CglsG!g{L5SSwEuiO8pC7+?QK%fMH zv!!S2_WzgM`6|)yY_m(?J(9cD_#Cs^*Il#}1d0(@Q?dEuhvbT4v9n` z5(J77_+j1tACN|h#mIRUnm(q+fGhkVcqik{PzRN zTZr&W67XZc)n4}W7yGw#ZprZ3c5(vWsoVdDq|=kP5aAai;KvDf)$RXxi^a}{A>iwZ zzPea*{c|IY1c71%M(ZB)#Jw^uD;7H&hQM`oC;z9U88i|EiV^r+-Twc$Sm10J0w!=z z-TvQBBSD}T0grj|>$?44QY>~h41w2oM-;5~;*SU8?a)XNC`RBS$z6-9^?#q_2Qyh9 zP>8_NiskoFxusCpY!(7t0$;4#|Ig4&5GYAtTgCo+z>~{M#?H2r6Y#|!cP*>7_&+GU zF?kCSen|pnO8=As5nf1IKIQM4oh-&g&8#s2%)WPw0A0_W5{;O(vDB4^7GkltPKfVaN$ zbv11SfszDnsJQrfM)G*aW&57+9yyJXCvg&B{u> z*Dt^Q=T(BhZ>6{7_TP{CdJ5eVY}sbz1a?X@bIbox$vrA7ZzjH82?Fj(bw+ymtG(J& zqQ%+fNdjBb4Rw6(0XYjmlj2kI?D0{LJ7)vj|3G_`1tZbfufAo_t{_ShN zl?kvf0p#~tPhT>uY@Pt}BY*Rf_YKf}T0WM%=#h}WuN%#;wEXVYm3%?|<~{ElAoBab zNB+KUG{5pt`poYht(!UFO-aC$^?WdQ(PR1DueJFsM}WNqEdK)JNB)!qke|%on-Y_( zL4M>0L_ac8v0uQCn z{FdJt;N=N{e@KKxBTwoF(5ziR;vV%ANgB#d&0Q3q@S4r`6ra5)eqby zy*7Q#PubI|-h<=vkTuDddDQ$_PkE5vgID-L=D{g~cbg=D{K(&=*uw<1K6%m89kz(f zKa48veTMPL$^7kEJ!C}4kNiWb(%#3Ae=#S#DbM$hN;jvk`8_?sk(909UM8PZL>`=f zIr2;9Fez?)xB&ugbf#r70pxE$=E;VD{QliT{>f_7#^;}vmZdLyT=FqJ7*Cq~%&*dyJyWI4S&QB#l^#B*ozj`P zD}Kwr75P&Vu>3QTKV{PopCMcQB=VE_TXlQFxE>_)Q}$&2;d8k=EqB>79r+W>@8A8* zi%)rWWNrNk3xl>l;LMNwKJjV0m5Ht|fqRi(LjJyPG{4e!_LDCzdyv0*&-(_5{K(%| zj0`K0f0ne1nm;9huO}~hx?P+0rfm8QxqWbqpGSV_5#&!vz(dv~U;Hc&@(<3Ct-g!= z$lt2l6UK%7$UmVZO@6>b6R%EQ^hl$UpW<(_eF2lmre(4G?&H*6;}X$e0?6;*J>(xONvrSvj?9nz zS%39@E}fIU=3h?PlhS{WR5mlc{A)SkMgFWufuCY-wX7ws0R+hWKH`yofFTXPbw6bf z@@M_w^WgkbvQ|4M<0kVroZbh7X8DuPOyNUdENki;AV!9_kl*reME;ZnzDMSlJV<+n zL`RITAi&`K&iuM-QQ7x|GtiR!&!B0uu?T9IT8)3P9c64iUd^i{s(i?8%<=7cvT z0pH(AzF2-=@9j+stZAMA@*{uqlD}Gj$dCN5=0@A={ov|~S9v@{-+n)E0j+rTZ z=ufBSPkG9N{FdJjvh7IO`hD|Zo|VWyF2A4X$~6DO*2n&L0o_fOJLr%9}gM%kw2~ck4dws`6+u+R(-B@JL%;gl{`I7+60(3a0ci1p?`(+4D#nA za8Nogeda$+x?Z|n+Q94oPGG(Ci@4+NXX)h!KsbT#$N7Ipc<_9I+ywkc#p7}QeBS)bpOelGjh&rC;Km^Nee(Mo>2w+Z0-*%H9q0eu zp@Fk=2sr)jiSvIo4FG{q0xrL!`W*VC!>1lB5C|kNU-CtmkUPJ==;TXJED#7J5I9Bn zPbJ@aV1Ym&fo(y_-@iES>&^m!Kmt>x{c-*;4UC*!L!kS-?F&Kvd%x$?1P}-%a9xo6 zE`A>EPGS-oJv+}%;I=sbw`XTh{P6@Hi1VLrpYhF~`x}f%2ZH?f9S24=&uw3P^d4#L zXpsND<4m{Dcmr~OgB3yY`{Z{I-9B@>S3~r7#`(XghWPmoi3zyd=k7TFZ%9m@&}$&z z$prr%uJl}x|L*qLNlc#5YasCcAo*SV@2A^m4F=@?4!6en@5jwp zAP`HyL(V!ChrIl_F$)9&2?S1c;clN-21d@VAs}54H`T>IrL$-P2!s;wQ|BR-k9SJ9 z&(M}<=O+kwvbiY#zeNi`Ae2DtR2M5lV`t|OcwO977ssSIGynub30xxC!;t#N&l>xQ zV-^Sm67Yj3A@X~tKL`w)T|=NtU{jp`x6uR;2qoZdpS}VamH$1~918>j3HaWV??8q4 z|2*A30~?-QcL})fLDW-UxBm^iAP`5u-F^LP@n`9VpKL2SqzH^Nkn837xpDa!Qc0asmZ(dPM@Bc?$a~#cZFi!OwZ}=umA4b zbEdmz%7n4&4sA1ayUEjbnfi{QX`h-gw8MMeH|5QT3{83e`-Zlkx!<8P4j$Tl#=(co zJYfH!%{Jb2?Ayjq7{Y(}|M;=;yzn0hkN^pg011!)36KB@kN^pg011!)36KB@kN^pg z011!)36KB@kN^pg011!)36KB@kN^pg011!)36KB@kN^pg011!)36KB@kU)O|H;61J zHem&kKXUDlCxL&9>}XS6qf?#Xne#N)c|9lcj{lboxibidwt`{Inw@P>OZg<9e|}gg z6GWbEOWj?NFJIi_HAEh73&seo;4+cr<5p1L)K(YyON(mlX_2-1mNUx9b6Z3*XcJf{ z^6DrxQk)1H;JnsV*|EiC4taG;5sbltDyId;(MJJP3gMJCR@PrdUTOpvPv5_3Bhnfh z9r}lvMzu34*$=h1v-;?$qDK64I`v;j;5#BOidY?klG;e*iNbu=*yQ7zy57za`ZAG+ z+Y7d0Ro)^3KIUN+kw3M%j_@Np>(1BAUA!hb||k6H->6Fa&aRr8d{>jyFjdtr>o72SZWZvybNq!8XK^54!>jiDoS zm(D=eG=cddD<+`+^+f*FrFyx~&Jl|0UeqPj1+CzIyC33wn_F4r*6!8FNAd{b9?$9? z?cP?fL}c5z*!!l25h*6d^k%R1XRfnzgpL!zi_X>)E-v7aedQSa5F+48Xe=g<4YYtT~07?%E23K5>^tyWTbN0Zp2+ziL78kcG*N* zyri)f863SVu9$xv*Ixa@z4qFeC-_)?A@fM1|6akDFtpcWpMQiDS5;`-1 z8E$y1ju)DN3fOwHm>**02*t#B^q7o{F+B=~%?7$Nw2z8xXoMx7><2ru zdq(ZT)*Jjzo_O42_+a()Vx!|jzK zcP0rZjwU0g^0|D!6Y&^Nmp3L>%fA-E&at*(d66F{h9$BfBPaJ~J4Yy9>qIVWL2{T{ zg9$d~0qT${}%~-u4oM|X$)V{RT}1JjXK9w z0#ACs-|ihpMikE8pQT;A5i7 z<6$P{SvJuR@{1K=D!2AFW#tIPlPQv}y*T&)8J@3g!$u;H1}IwXtY{Df;0WEqNG{l( zy51_dO9UTDv<+O+SZ|$1u!iXYCSk=|cR9vRkDHnlBbO+aG+tbHqmCeejwkWB$eOuI zjCpuMeQNnw%zO_>i%S|C+LS&sph82RUc07cfZs^qRTq{tVsT|d1<>(q8#+RNTv;=} zv-9F+{R5UX{=ITRIxa%n;(s8k%eV(SRFe%B(!L>XhI>=PlE#~=mBO~_nK}sk(r5?) zcV+2s{a}V0Mjf^R`R9_x7PO$Hc8<_X%XXfhx0U{I>IVjUk=FuIHLD2c7|H3PQu@bg zdiWb{1D7;5OX0=2g(LJe7J9<>c8fFCd-X2~e6DCM5lpn}%^S}&m%x$H^^fuMVowz< z5qW3ye2z*GOB%27%yx5C(Vd*b5gK`8*jI~Sm7*8n14d~v`k$D*bf#yXy$Z<4h6JX? zl@mPYPy1-;3L`&No(h&U!u4cWM@%{#f%$O-HpxdzP@VZCAACs%bB)6Br)66LaD;B2 z@9Y8}ALXMZyklK0aDIx-V@V^1h_)5P)I$^aRpWD?upA(tuE7@A(pQO%e=njhZLlcPO2!-?KVINUVuT{@p#<-+0wB(Yqzc7j; z6wV)P!=fxW9!#*b&?In4qq2_L6m7TgQiCrA3owEGsVeNOEb@DG-da}UEKZ)lLJ@4#6Cz*=v3|n@=s$u#YNVKW zEB9TBp9EeYFg1kwMJD<~Mr`7C?Nb`Sh{31;{|J$=7p9qfezYLcO(QGK3@adLPv1_lmXdGg$b> zC5^SHrF}*o6Is1(yBRGG~ROd&TZANPofx~OnKQ@}s&-{tt{K3ppq|&DP zVp-}Xa8Ip{(CvNnhL#f4`7@qN8rk+01a__OCt#+J{{LBDy1kY#u3Mxid%c|rtu>Lr zyk7N`J#&VSRH>@BGJ}SM`SFxTLXc)pw+aFD@qD##VHMe#M9XZ|YUQ z$Sj}Ki&5}uHwj!_O_P`VC~$7Iv_{>%lh30^H!GK=b`rqTYn6RyZ&m9b55LEbQgDSo zxunrqvy7l5P}&i?lE{5N`mf9R$t8`-nq@Q%0XRajdtr$%&gX*{=>NAVkw(ug_+kZ@ zG-|4qar6WZ?U9-0#0EYKtezG@{OVzUE@@Q%=IRB|@tAj)a)eItks{Uz;P;|p*vhBH zb-Yeld_4g;LLp|~aH@~~7mDC{p-Olimo&2H4@TfBe`#V8DH@Kll_7GKz$J}J&Wu)$ z034y4_y`o8Il6RZT4*Ntv8WQhu1-stB}OBGQ3THR5hw=pUdw(YoG>Ab=(9t7V;XzXLdb^!mTIpM}2X&P$-|;d&KXZVG|B`l>T`@*E$b z&dc3ce7L~JY*Sz|pN%8%!(y3=%=Z!M=`L#{*Iz2~jE~vIQDugkLIBh6i^H92p<8(m~%y8IuG85je;tRt(hfRI5z=ZkM%@>yy+5mFY4X z36vu+*`?SyYOSJ~I2Tclr1ojG&2oj}T_m6(fa#Z5fmjeQn7PJBs}~g5%3ad8DNQLD zhXl$Iz(SQVIa&4|;$aq>A}Woe|3AFfM87lK@KA}A(Sj1Vw4XTh=ivOornt&H;MZi& z{LKT7@u5AqiIg@OjRe9GIDUXcv(sMAAG}H^ew8+2c0-?~l;O%w%Oo%@6Z(LI&-iHd z{sBfto#uvz6kZAhF#T-(QPS-B^CX{v3P$ysaH5aR6hJW#tt5cy$1A$@dby8QmChe8 zo2?|zLZv|9T9^27eUV!KdgsprYMUugVjkK`;7phPSMqWGU^Jz1{;bJq|7`wg5cs4^ z{}Vm)sd4^n?`gBGoe#@pO8~=_x4ZPekB{@`S(mku>)i0rw%(cg2!W?WUg=WoaXx)D z<}+);c|JB-StJmMz#kQ2z0yak7b^@R$^svofyz#^B=CKOTroRH2@0br=R9g8IM(NZ zTA4JXkw6Ioc(Gd?;QY}qQNgmu-HOc(U*(2}C2ESdgdnh!ORjMKpjTHWjhOgAhBHi8 z8&sytXe3aA06O*|mssKa(eKamgv;8<_0y<-5=el+gRTt^A=hJl`v2Q>`k$Xb*-La6 zyrpFXey)%!{5kqkV>1(H7$fp8ADjBr8IuIO2z=QoRuQi~-@c$Qhz$?%nCz8LMkav} z1U~ALE1W<2J>Q_}LtF-Nof{s8s7l)OB>=f@;*u+zKX{~7T88sySC>ItpRG2iOqbC} zpag-(TpJ!jt8o5cK9ABkrc!LA(EmkhgUWOnjRZ;%xJjW^FXzuaA{g5{!$Rtx1QHQ(T%+b)B%mR%*tOvy8pj4c zpJm5XB8~Hh8y;$EhjBs^_=&6Y$MtP|Uev;y>U91rF9IKF=%Ud+2{;KHr_evf>KN5H zO^rmrYhtJK=Pf=4mFY4X36vl(RiXb=eB`=7VQ>$1YbsG9-a-OO0+`S5T%`i%kC*cw zo4GjyH#}5U5TgYpfRA&n=n^ZOKj_kx*}yRAD=ve$jy9q+$Y>-GjsU*o;tX*9JffB> zoIf+27Dc$>VYoV_Wj%p26k3JzN5AaxNfGSlRvcdA^E9C^3uBUiFM%1wN*%t6{u~pe zm6kD}g1n0ZH#}6rWVE0JFjk*XtkmJFnE9#Kzh)CHZg?29T4`KK0P9(t8y=zs%vE>( zNcRdwgW`sV$_iq%pakwvXcbXz@X^1<`J?wH1uZ^}cbve5I{p7$t$z%YoX($>d^VBm zcm=WeK?xkG&_5n#_0AvY-|76>T-}<2mYv2)U`K`ip=bTW?^1>HXFByy0tpaUBC@tZ z{}|6e)ylr&VTHld)Yg=VG8ze#Ab>CPViK}5%<}2~uXCCc`6cyF0tpcKnL?|G0{tud zf4og_zD*wEGnJx5qvkCnpd@gzPX8GHU_QUbCf+7Eoj>bv=7SEV1~}tXA+V=T|9Jkz zM}aj7VLhYM`Lm18lqNaGA%W5a;L_17(M1!1{}aCySkhRW*5kv`5V<%U?Xpvx3a^qt zEd(&1dF5iI=B~b5Z6g01a5RHH5UObWw&94_x%;XNr)d2 zG!1GY`+pKhfWSF9d20@#ZFKJ#2;o@(kHhHj@ec?16DM&=BmP(L;WvS*uh?_8R#%*-U#J_)2jKr>d)bX(WKerc4w9X%LJQJTQhKAm=5 z;)k6%)ISL%Kwyz#!^0Y7pP9l7g3@i4HgjLL|fwlH#}@d z|Jn3X{HjJx`b>*nnwP)y)(3;l1NK*KM_!mAx2u!DvEx#3|))RlNKmx!!oq5rS3|0jV2 z2s|ZnyvQ*kN9X>H{Fy(Aov1T>^ literal 0 HcmV?d00001 diff --git a/examples/y.ppm b/examples/y.ppm new file mode 100644 index 0000000000000000000000000000000000000000..238c0137c48afcf0e31cf5091d274b71c04dce8c GIT binary patch literal 196668 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 AyZ`_I literal 0 HcmV?d00001 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..0651087 --- /dev/null +++ b/main.cpp @@ -0,0 +1,408 @@ +/////////////////////////////////////////////////////////////////////////////////// +// File : main.cpp +/////////////////////////////////////////////////////////////////////////////////// +// +// LumosQuad - A Lightning Generator +// Copyright 2007 +// The University of North Carolina at Chapel Hill +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. +// +// Permission to use, copy, modify and distribute this software and its +// documentation for educational, research and non-profit purposes, without +// fee, and without a written agreement is hereby granted, provided that the +// above copyright notice and the following three paragraphs appear in all +// copies. +// +// THE UNIVERSITY OF NORTH CAROLINA SPECIFICALLY DISCLAIM ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN +// "AS IS" BASIS, AND THE UNIVERSITY OF NORTH CAROLINA HAS NO OBLIGATION TO +// PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +// +// Please send questions and comments about LumosQuad to kim@cs.unc.edu. +// +/////////////////////////////////////////////////////////////////////////////////// +// +// This program uses OpenEXR, which has the following restrictions: +// +// Copyright (c) 2002, Industrial Light & Magic, a division of Lucas +// Digital Ltd. LLC +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Industrial Light & Magic nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +/////////////////////////////////////////////////////////////////////////////////// +/// +/// \mainpage Fast Animation of Lightning Using An Adaptive Mesh +/// \section Introduction +/// +/// This project is an implementation of the paper +/// Fast Animation of Lightning Using An Adaptive Mesh. It +/// includes both the simulation and rendering components described in that paper. +/// +/// Several pieces of software are used in this project that the respective +/// authors were kind enough to make freely available: +/// +///
    +///
  • Mersenne twister +/// (Thanks to Makoto Matsumoto) +///
  • FFTW +/// (Thanks to Matteo Frigo and Steven G. Johnson) +///
  • OpenEXR +/// (Thanks to ILM) +///
  • GLVU +/// (Thanks to Walkthru) +///
  • Antimony +/// (Thanks to Daniel Dunbar and Greg Humphreys) +///
+/// +/// Theodore Kim, kim@cs.unc.edu, October 2006 +/// +/////////////////////////////////////////////////////////////////////////////////// + +#define COMMAND_LINE_VERSION 1 + +#include +#include "ppm/ppm.hpp" +#include "APSF.h" +#include "FFT.h" +#include "QUAD_DBM_2D.h" +#include "EXR.h" + +using namespace std; + +//////////////////////////////////////////////////////////////////////////// +// globals +//////////////////////////////////////////////////////////////////////////// +int iterations = 10; +static QUAD_DBM_2D* potential = new QUAD_DBM_2D(256, 256, iterations); +APSF apsf(512); + +// input image info +int inputWidth = -1; +int inputHeight = -1; + +// input params +string inputFile; +string outputFile; + +// image scale +float scale = 5; + +// pause the simulation? +bool pause = false; + +//////////////////////////////////////////////////////////////////////////// +// render the glow +//////////////////////////////////////////////////////////////////////////// +void renderGlow(string filename, int scale = 1) +{ + int w = potential->xDagRes() * scale; + int h = potential->yDagRes() * scale; + + // draw the DAG + float*& source = potential->renderOffscreen(scale); + + // if there is no input dimensions specified, else there were input + // image dimensions, so crop it + if (inputWidth == -1) + { + inputWidth = potential->inputWidth(); + inputHeight = potential->inputHeight(); + } + + // copy out the cropped version + int wCropped = inputWidth * scale; + int hCropped = inputHeight * scale; + float* cropped = new float[wCropped * hCropped]; + cout << endl << " Generating EXR image width: " << wCropped << " height: " << hCropped << endl; + for (int y = 0; y < hCropped; y++) + for (int x = 0; x < wCropped; x++) + { + int uncroppedIndex = x + y * w; + int croppedIndex = x + y * wCropped; + cropped[croppedIndex] = source[uncroppedIndex]; + } + + // create the filter + apsf.generateKernelFast(); + + // convolve with FFT + bool success = FFT::convolve(cropped, apsf.kernel(), wCropped, hCropped, apsf.res(), apsf.res()); + + if (success) { + EXR::writeEXR(filename.c_str(), cropped, wCropped, hCropped); + cout << " " << filename << " written." << endl; + } + else + cout << " Final image generation failed." << endl; + + delete[] cropped; +} + +//////////////////////////////////////////////////////////////////////////// +// load image file into the DBM simulation +//////////////////////////////////////////////////////////////////////////// +bool loadImages(string inputFile) +{ + // load the files + unsigned char* input = NULL; + LoadPPM(inputFile.c_str(), input, inputWidth, inputHeight); + + unsigned char* start = new unsigned char[inputWidth * inputHeight]; + unsigned char* repulsor = new unsigned char[inputWidth * inputHeight]; + unsigned char* attractor = new unsigned char[inputWidth * inputHeight]; + unsigned char* terminators = new unsigned char[inputWidth * inputHeight]; + + // composite RGB channels into one + for (int x = 0; x < inputWidth * inputHeight; x++) + { + start[x] = (input[3 * x] == 255) ? 255 : 0; + repulsor[x] = (input[3 * x + 1] == 255) ? 255 : 0; + attractor[x] = (input[3 * x + 2] == 255) ? 255 : 0; + terminators[x] = 0; + + if (input[3 * x] + input[3 * x + 1] + input[3 * x + 2] == 255 * 3) + { + terminators[x] = 255; + start[x] = repulsor[x] = attractor[x] = 0; + } + } + + if (potential) delete potential; + potential = new QUAD_DBM_2D(inputWidth, inputHeight, iterations); + bool success = potential->readImage(start, attractor, repulsor, terminators, inputWidth, inputHeight); + + // delete the memory + delete[] input; + delete[] start; + delete[] repulsor; + delete[] attractor; + delete[] terminators; + + return success; +} + +int width = 600; +int height = 600; +bool animate = false; +float camera[2]; +float translate[2]; + +//////////////////////////////////////////////////////////////////////////// +// window Reshape function +//////////////////////////////////////////////////////////////////////////// +void Reshape(int w, int h) +{ + if (h == 0) h = 1; + + glViewport(0, 0, w, h); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + gluOrtho2D(-camera[0] - translate[0], camera[0] + translate[0], -camera[1] - translate[1], camera[1] + translate[1]); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); +} + +//////////////////////////////////////////////////////////////////////////// +// GLUT Display callback +//////////////////////////////////////////////////////////////////////////// +void Display() +{ + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + gluOrtho2D(-camera[0] + translate[0], camera[0] + translate[0], -camera[1] + translate[1], camera[1] + translate[1]); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + potential->draw(); + potential->drawSegments(); + + glutSwapBuffers(); +} + +//////////////////////////////////////////////////////////////////////////// +// GLUT Keyboard callback +//////////////////////////////////////////////////////////////////////////// +void Keyboard(unsigned char key, int x, int y) +{ + switch(key) + { + case 'p': + pause = !pause; + break; + + case 'q': + cout << " You terminated the simulation prematurely." << endl; + exit(0); + break; + } + + glutPostRedisplay(); +} + +//////////////////////////////////////////////////////////////////////////// +// window Reshape function +//////////////////////////////////////////////////////////////////////////// +void Idle() +{ + if (!pause) + for (int x = 0; x < 100; x++) + { + bool success = potential->addParticle(); + + if (!success) + { + cout << " No nodes left to add! Is your terminator reachable?" << endl; + //exit(1); + return; + } + + if (potential->hitGround()) + { + glutPostRedisplay(); + cout << endl << endl; + + // write out the DAG file + string lightningFile = inputFile.substr(0, inputFile.size() - 3) + string("lightning"); + cout << " Intermediate file " << lightningFile << " written." << endl; + potential->writeDAG(lightningFile.c_str()); + + // render the final EXR file + renderGlow(outputFile, scale); + delete potential; + exit(0); + } + } + glutPostRedisplay(); +} + +//////////////////////////////////////////////////////////////////////////// +// GLUT Main +//////////////////////////////////////////////////////////////////////////// +int glutMain(int *argc, char **argv) +{ + float smaller = 1.0f; + camera[0] = smaller * 0.5f; + camera[1] = smaller * 0.5f; + translate[0] = 0.0f; + translate[1] = 0.0f; + + glutInit(argc, argv); + glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGBA); + glutInitWindowPosition(50, 50); + glutInitWindowSize(width, height); + glutCreateWindow("Lumos: A Lightning Generator v0.1"); + + glutDisplayFunc(Display); + glutKeyboardFunc(Keyboard); + glutIdleFunc(Idle); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA,GL_ONE); + + Reshape(width, height); + + glClearColor(0.0, 0.0, 0.0, 1.0); + glShadeModel(GL_SMOOTH); + + // Go! + glutMainLoop(); + + return 0; +} + +//////////////////////////////////////////////////////////////////////////// +// Main +//////////////////////////////////////////////////////////////////////////// +int main(int argc, char **argv) +{ + if (argc < 3) + { + cout << endl; + cout << " LumosQuad " << endl; + cout << " =========================================================" << endl; + cout << " - *.ppm file with input colors" << endl; + cout << " --OR--" << endl; + cout << " *.lightning file from a previous run" << endl; + cout << " - The OpenEXR file to output" << endl; + cout << " - Scaling constant for final image." << endl; + cout << " Press 'q' to terminate the simulation prematurely." << endl; + cout << " Send questions and comments to kim@cs.unc.edu" << endl; + return 1; + } + + cout << endl << "Lumos: A lightning generator v0.1" << endl; + cout << "------------------------------------------------------" << endl; + + // store the input params + inputFile = string(argv[1]); + outputFile = string(argv[2]); + if (argc > 3) scale = atoi(argv[3]); + + // see if the input is a *.lightning file + if (inputFile.size() > 10) + { + string postfix = inputFile.substr(inputFile.size() - 9, inputFile.size()); + + cout << " Using intermediate file " << inputFile << endl; + if (postfix == string("lightning")) + { + potential->readDAG(inputFile.c_str()); + renderGlow(outputFile, scale); + delete potential; + return 0; + } + } + + // read in the *.ppm input file + if (!loadImages(inputFile)) + { + cout << " ERROR: " << inputFile.c_str() << " is not a valid PPM file." << endl; + return 1; + } + cout << " " << inputFile << " read." << endl << endl; + + + // loop simulation until it hits a terminator + cout << " Total particles added: "; + glutMain(&argc, argv); + + return 0; +} diff --git a/ppm/ppm.cpp b/ppm/ppm.cpp new file mode 100644 index 0000000..c95b50d --- /dev/null +++ b/ppm/ppm.cpp @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +// File : ppm.cpp +//------------------------------------------------------------------------------ +// GLVU : Copyright 1997 - 2002 +// The University of North Carolina at Chapel Hill +//------------------------------------------------------------------------------ +// Permission to use, copy, modify, distribute and sell this software and its +// documentation for any purpose is hereby granted without fee, provided that +// the above copyright notice appear in all copies and that both that copyright +// notice and this permission notice appear in supporting documentation. +// Binaries may be compiled with this software without any royalties or +// restrictions. +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. + +//============================================================================ +// ppm.cpp : Portable Pixel Map image format module +//============================================================================ + +#include + +#define PPM_VERBOSE 0 + +//---------------------------------------------------------------------------- +// READS AN IMAGE IN FROM A PPM FILE. RETURNS THE COLOR RGB ARRAY AND DIMENSIONS +// PERFORMS AUTO-ALLOCATION OF Color ARRAY IF SET TO NULL BEFORE CALLING; OTHERWISE +// ASSUMES THAT COLOR HAS BEEN PRE-ALLOCED. +//---------------------------------------------------------------------------- +void LoadPPM(const char *FileName, unsigned char* &Color, int &Width, int &Height) +{ + FILE* fp = fopen(FileName, "rb"); + if (fp==NULL) + { printf("PPM ERROR (ReadPPM) : unable to open %s!\n",FileName); + Color=NULL; Width=0; Height=0; return; } + int c,s; + do{ do { s=fgetc(fp); } while (s!='\n'); } while ((c=fgetc(fp))=='#'); + ungetc(c,fp); + fscanf(fp, "%d %d\n255\n", &Width, &Height); +#if PPM_VERBOSE + printf("Reading %dx%d Texture [%s]. . .\n", Width, Height, FileName); +#endif + int NumComponents = Width*Height*3; + if (Color==NULL) Color = new unsigned char[NumComponents]; + fread(Color,NumComponents,1,fp); + fclose(fp); +} + +//---------------------------------------------------------------------------- +// Writes an unsigned byte RGB color array out to a PPM file. +//---------------------------------------------------------------------------- +void WritePPM(const char *FileName, unsigned char* Color, int Width, int Height) +{ + FILE* fp = fopen(FileName, "wb"); + if (fp==NULL) { printf("PPM ERROR (WritePPM) : unable to open %s!\n",FileName); return; } + fprintf(fp, "P6\n%d %d\n255\n", Width, Height); + fwrite(Color,1,Width*Height*3,fp); + fclose(fp); +} diff --git a/ppm/ppm.hpp b/ppm/ppm.hpp new file mode 100644 index 0000000..c887b0f --- /dev/null +++ b/ppm/ppm.hpp @@ -0,0 +1,23 @@ +//------------------------------------------------------------------------------ +// File : ppm.hpp +//------------------------------------------------------------------------------ +// GLVU : Copyright 1997 - 2002 +// The University of North Carolina at Chapel Hill +//------------------------------------------------------------------------------ +// Permission to use, copy, modify, distribute and sell this software and its +// documentation for any purpose is hereby granted without fee, provided that +// the above copyright notice appear in all copies and that both that copyright +// notice and this permission notice appear in supporting documentation. +// Binaries may be compiled with this software without any royalties or +// restrictions. +// +// The University of North Carolina at Chapel Hill makes no representations +// about the suitability of this software for any purpose. It is provided +// "as is" without express or implied warranty. + +//============================================================================ +// ppm.hpp : Portable Pixel Map image format module +//============================================================================ + +void LoadPPM(const char *FileName, unsigned char* &Color, int &Width, int &Height); +void WritePPM(const char *FileName, unsigned char* Color, int Width, int Height);