diff --git a/Videos/OneLoneCoder_PGE_Dithering.cpp b/Videos/OneLoneCoder_PGE_Dithering.cpp new file mode 100644 index 0000000..ae1f688 --- /dev/null +++ b/Videos/OneLoneCoder_PGE_Dithering.cpp @@ -0,0 +1,265 @@ +/* + Blithering About Dithering (Floyd-Steinberg) + "2022 lets go!" - javidx9 + + License (OLC-3) + ~~~~~~~~~~~~~~~ + Copyright 2018 - 2021 OneLoneCoder.com + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions or derivations of source code must retain the above + copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions or derivative works in binary form must reproduce + the above copyright notice. This list of conditions and the following + disclaimer must be reproduced in the documentation and/or other + materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may 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 + HOLDER 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. + + Video: + ~~~~~~ + https://youtu.be/lseR6ZguBNY + + Use Q and W keys to view quantised image and dithered image respectively + Use Left mouse button to pan, and mouse wheel to zoom to cursor + + Links + ~~~~~ + YouTube: https://www.youtube.com/javidx9 + https://www.youtube.com/javidx9extra + Discord: https://discord.gg/WhwHUMV + Twitter: https://www.twitter.com/javidx9 + Twitch: https://www.twitch.tv/javidx9 + GitHub: https://www.github.com/onelonecoder + Homepage: https://www.onelonecoder.com + + Author + ~~~~~~ + David Barr, aka javidx9, ŠOneLoneCoder 2019, 2020, 2021, 2022 +*/ + + +// Using olc::PixelGameEngine for input and visualisation +#define OLC_PGE_APPLICATION +#include "olcPixelGameEngine.h" + +// Using a transformed view to handle pan and zoom +#define OLC_PGEX_TRANSFORMEDVIEW +#include "olcPGEX_TransformedView.h" + +//#include + +// Override base class with your custom functionality +class Dithering : public olc::PixelGameEngine +{ +public: + Dithering() + { + sAppName = "Floyd Steinberg Dithering"; + } + + olc::TransformedView tv; + std::unique_ptr m_pImage; + std::unique_ptr m_pQuantised; + std::unique_ptr m_pDithered; + +public: + // Called once at start of application + bool OnUserCreate() override + { + // Prepare Pan & Zoom + tv.Initialise({ ScreenWidth(), ScreenHeight() }); + + // Load Test Image + m_pImage = std::make_unique("./assets/Flower_640x480.png"); + + // Create two more images with the same dimensions + m_pQuantised = std::make_unique(m_pImage->width, m_pImage->height); + m_pDithered = std::make_unique(m_pImage->width, m_pImage->height); + + // These lambda functions output a new olc::Pixel based on + // the pixel it is given + auto Convert_RGB_To_Greyscale = [](const olc::Pixel in) + { + uint8_t greyscale = uint8_t(0.2162f * float(in.r) + 0.7152f * float(in.g) + 0.0722f * float(in.b)); + return olc::Pixel(greyscale, greyscale, greyscale); + }; + + + // Quantising functions + auto Quantise_Greyscale_1Bit = [](const olc::Pixel in) + { + return in.r < 128 ? olc::BLACK : olc::WHITE; + }; + + auto Quantise_Greyscale_NBit = [](const olc::Pixel in) + { + constexpr int nBits = 2; + constexpr float fLevels = (1 << nBits) - 1; + uint8_t c = uint8_t(std::clamp(std::round(float(in.r) / 255.0f * fLevels) / fLevels * 255.0f, 0.0f, 255.0f)); + return olc::Pixel(c, c, c); + }; + + auto Quantise_RGB_NBit = [](const olc::Pixel in) + { + constexpr int nBits = 2; + constexpr float fLevels = (1 << nBits) - 1; + uint8_t cr = uint8_t(std::clamp(std::round(float(in.r) / 255.0f * fLevels) / fLevels * 255.0f, 0.0f, 255.0f)); + uint8_t cb = uint8_t(std::clamp(std::round(float(in.g) / 255.0f * fLevels) / fLevels * 255.0f, 0.0f, 255.0f)); + uint8_t cg = uint8_t(std::clamp(std::round(float(in.b) / 255.0f * fLevels) / fLevels * 255.0f, 0.0f, 255.0f)); + return olc::Pixel(cr, cb, cg); + }; + + auto Quantise_RGB_CustomPalette = [](const olc::Pixel in) + { + std::array nShades = { olc::BLACK, olc::WHITE, olc::YELLOW, olc::MAGENTA, olc::CYAN }; + + float fClosest = INFINITY; + olc::Pixel pClosest; + + for (const auto& c : nShades) + { + float fDistance = float( + std::sqrt( + std::pow(float(c.r) - float(in.r), 2) + + std::pow(float(c.g) - float(in.g), 2) + + std::pow(float(c.b) - float(in.b), 2))); + + if (fDistance < fClosest) + { + fClosest = fDistance; + pClosest = c; + } + } + + return pClosest; + }; + + + // We don't need greyscale for the final demonstration, which uses + // RGB, but I've left this here as reference + //std::transform( + // m_pImage->pColData.begin(), + // m_pImage->pColData.end(), + // m_pImage->pColData.begin(), Convert_RGB_To_Greyscale); + + + // Quantise The Image + std::transform( + m_pImage->pColData.begin(), + m_pImage->pColData.end(), + m_pQuantised->pColData.begin(), Quantise_RGB_NBit); + + // Perform Dither + Dither_FloydSteinberg(m_pImage.get(), m_pDithered.get(), Quantise_RGB_NBit); + + return true; + } + + + + void Dither_FloydSteinberg(const olc::Sprite* pSource, olc::Sprite* pDest, + std::function funcQuantise) + { + // The destination image is primed with the source image as the pixel + // values become altered as the algorithm executes + std::copy(pSource->pColData.begin(), pSource->pColData.end(), pDest->pColData.begin()); + + // Iterate through each pixel from top left to bottom right, compare the pixel + // with that on the "allowed" list, and distribute that error to neighbours + // not yet computed + olc::vi2d vPixel; + for (vPixel.y = 0; vPixel.y < pSource->height; vPixel.y++) + { + for (vPixel.x = 0; vPixel.x < pSource->width; vPixel.x++) + { + // Grap and get nearest pixel equivalent from our allowed + // palette + olc::Pixel op = pDest->GetPixel(vPixel); + olc::Pixel qp = funcQuantise(op); + + // olc::Pixels are "inconveniently" clamped to sensible ranges using an unsigned type... + // ...which means they cant be negative. This hampers us a tad here, + // so will resort to manual alteration using a signed type + int32_t error[3] = + { + op.r - qp.r, + op.g - qp.g, + op.b - qp.b + }; + + // Set destination pixel with nearest match from quantisation function + pDest->SetPixel(vPixel, qp); + + // Distribute Error - Using a little utility lambda to keep the messy code + // all in one place. It's important to allow pixels to temporarily become + // negative in order to distribute the error to the neighbours in both + // directions... value directions that is, not spatial! + auto UpdatePixel = [&vPixel, &pDest, &error](const olc::vi2d& vOffset, const float fErrorBias) + { + olc::Pixel p = pDest->GetPixel(vPixel + vOffset); + int32_t k[3] = { p.r, p.g, p.b }; + k[0] += int32_t(float(error[0]) * fErrorBias); + k[1] += int32_t(float(error[1]) * fErrorBias); + k[2] += int32_t(float(error[2]) * fErrorBias); + pDest->SetPixel(vPixel + vOffset, olc::Pixel(std::clamp(k[0], 0, 255), std::clamp(k[1], 0, 255), std::clamp(k[2], 0, 255))); + }; + + UpdatePixel({ +1, 0 }, 7.0f / 16.0f); + UpdatePixel({ -1, +1 }, 3.0f / 16.0f); + UpdatePixel({ 0, +1 }, 5.0f / 16.0f); + UpdatePixel({ +1, +1 }, 1.0f / 16.0f); + } + } + } + + // Called every frame + bool OnUserUpdate(float fElapsedTime) override + { + // Handle Pan & Zoom using defaults middle mouse button + tv.HandlePanAndZoom(0); + + // Erase previous frame + Clear(olc::BLACK); + + // Draw Source Image + if (GetKey(olc::Key::Q).bHeld) + { + tv.DrawSprite({ 0,0 }, m_pQuantised.get()); + } + else if (GetKey(olc::Key::W).bHeld) + { + tv.DrawSprite({ 0,0 }, m_pDithered.get()); + } + else + { + tv.DrawSprite({ 0,0 }, m_pImage.get()); + } + + return true; + } +}; + +int main() +{ + Dithering demo; + if (demo.Construct(1280, 720, 1, 1)) + demo.Start(); + return 0; +} \ No newline at end of file