diff --git a/dev/msvc/olcPGE3.vcxproj b/dev/msvc/olcPGE3.vcxproj index ab6ff8e..066e68d 100644 --- a/dev/msvc/olcPGE3.vcxproj +++ b/dev/msvc/olcPGE3.vcxproj @@ -182,6 +182,7 @@ + true true diff --git a/dev/msvc/olcPGE3.vcxproj.filters b/dev/msvc/olcPGE3.vcxproj.filters index 04da150..b68a605 100644 --- a/dev/msvc/olcPGE3.vcxproj.filters +++ b/dev/msvc/olcPGE3.vcxproj.filters @@ -201,6 +201,9 @@ Extensions\MiniAudio + + Utilities + diff --git a/dev/msvc/olcPGE3_BuildSH/olcPGE3_BuildSH.vcxproj b/dev/msvc/olcPGE3_BuildSH/olcPGE3_BuildSH.vcxproj index f044f79..46c3503 100644 --- a/dev/msvc/olcPGE3_BuildSH/olcPGE3_BuildSH.vcxproj +++ b/dev/msvc/olcPGE3_BuildSH/olcPGE3_BuildSH.vcxproj @@ -107,6 +107,10 @@ true true + + false + false + true true @@ -164,8 +168,8 @@ true true - false - false + true + true true @@ -242,15 +246,19 @@ $(SolutionDir)..\..\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);../../../; $(SolutionDir)..\..\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);../../../; $(SolutionDir)..\..\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);../../../; $(SolutionDir)..\..\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);../../../; diff --git a/dev/msvc/olcPGE3_BuildSH/olcPGE3_BuildSH.vcxproj.filters b/dev/msvc/olcPGE3_BuildSH/olcPGE3_BuildSH.vcxproj.filters index 1da31ea..aafb310 100644 --- a/dev/msvc/olcPGE3_BuildSH/olcPGE3_BuildSH.vcxproj.filters +++ b/dev/msvc/olcPGE3_BuildSH/olcPGE3_BuildSH.vcxproj.filters @@ -99,6 +99,9 @@ Source Files + + Source Files + diff --git a/dev/src/image.cpp b/dev/src/image.cpp index b54fa07..0e67566 100644 --- a/dev/src/image.cpp +++ b/dev/src/image.cpp @@ -72,6 +72,37 @@ namespace olc return onCPU; } + bool Image::set(int x, int y, olc::Pixel p) + { + if (x >= 0 && y >= 0 && x < dimensions.x && y < dimensions.y) + { + pixels[int(y) * dimensions.x + int(x)] = p; + return true; + } + else + return false; + } + + olc::Pixel Image::get(int x, int y) + { + if (x >= 0 && y >= 0 && x < dimensions.x && y < dimensions.y) + { + return pixels[int(y) * dimensions.x + int(x)]; + } + else + return olc::Colour::BLACK; + } + + int Image::width() + { + return dimensions.x; + } + + int Image::height() + { + return dimensions.y; + } + olc::ImageRegion Image::all() { return olc::ImageRegion(*this); diff --git a/dev/src/image.h b/dev/src/image.h index 0f57c0f..0c1263e 100644 --- a/dev/src/image.h +++ b/dev/src/image.h @@ -82,6 +82,13 @@ namespace olc bool BoundToGPU() const; bool BoundToCPU() const; + + // SW Renderer IFace + bool set(int x, int y, olc::Pixel p); + olc::Pixel get(int x, int y); + int width(); + int height(); + public: olc::ImageRegion all(); olc::ImageRegion region(const olc::vf2d pos, const olc::vf2d& size); diff --git a/examples/olcPGE3_SoftwareRendering.cpp b/examples/olcPGE3_SoftwareRendering.cpp new file mode 100644 index 0000000..9a5a4bc --- /dev/null +++ b/examples/olcPGE3_SoftwareRendering.cpp @@ -0,0 +1,200 @@ +/* + olc::PixelGameEngine3 Example - Software Rendering + + Draws transformed and textured graphics entirely with the CPU + + Licenced under the OLC-3 License +*/ + + +// Define OLC_PGE3_APPLICATION to include the implementation of +// the Pixel Game Engine as part of this translation unit +#define OLC_PGE3_APPLICATION +#include "../olcPixelGameEngine3.h" +#include "../utilities/olcUTIL3_SWRender.h" + +struct TextSurface +{ + TextSurface() + { + sSurface.resize(vSize.area(), '-'); + } + + std::string sSurface; + olc::vi2d vSize = { 32, 30 }; + + bool set(int x, int y, char c) + { + if (x >= 0 && y >= 0 && x < vSize.x && y < vSize.y) + { + sSurface[y * vSize.x + x] = c; + return true; + } + else + return false; + } + + char get(int x, int y) + { + if (x >= 0 && y >= 0 && x < vSize.x && y < vSize.y) + { + return sSurface[y * vSize.x + x]; + } + else + return 'x'; + } + + int width() { return vSize.x; } + int height() { return vSize.y; } +}; + + + +class Example_SoftwareRenderer : public olc::PixelGameEngine +{ +public: + Example_SoftwareRenderer() + { + sAppName = "Example - Lines"; + } + +public: + // Called once at the start, so create things here + bool OnUserCreate() override + { + // Nothing to do here, so return true + CreateImageFromFile(imgTest, "./assets/sanity_texture.png"); + return true; + } + + // Called every frame, so update things here + bool OnUserUpdate(float fElapsedTime) override + { + + draw.Clear(olc::Colour::CYAN); + draw.Pixel({ 0,0 },olc::Colour::GREEN); + + DrawSW.SetTarget(draw.GetTarget()); + + DrawSW.Clear(olc::Colour::VERY_DARK_BLUE); + + DrawSW.Plot({ 4, 4 }, olc::Colour::RED); + + DrawSW.Line({ 10,4 }, mouse.GetPosition(), olc::Colour::MAGENTA, olc::Colour::VERY_DARK_BLUE); + + DrawSW.FilledRect({ 10,22 }, { 32, 32 }, olc::Colour::BLACK); + + DrawSW.FilledRect({ 6, 18 }, { 32, 32 }, + olc::Colour::RED, olc::Colour::YELLOW, + olc::Colour::GREEN, olc::Colour::MAGENTA); + + DrawSW.Rect({ 6, 18 }, { 32, 32 }, olc::Colour::WHITE); + + + + float fTotal = float(TotalTimeElapsed()); + + DrawSW.WorldOffset({ 68, 68 }); + DrawSW.WorldRotate(fTotal, { 0 ,0 }); + DrawSW.FilledRect({ -16, -16 }, { 32, 32 }, olc::Colour::BLACK); + + DrawSW.WorldReset(); + DrawSW.WorldOffset({ 64, 64 }); + DrawSW.WorldRotate(fTotal, { 0 ,0 }); + DrawSW.FilledRect({ -16, -16 }, { 32, 32 }, + olc::Colour::RED, olc::Colour::YELLOW, + olc::Colour::GREEN, olc::Colour::MAGENTA); + + DrawSW.Rect({ -16, -16 }, { 32, 32 }, olc::Colour::WHITE); + + + DrawSW.WorldReset(); + DrawSW.WorldOffset({ 128, 68 }); + DrawSW.WorldRotate(fTotal, { 0 ,0 }); + DrawSW.WorldScale({ 2, 2 }); + DrawSW.FilledRect({ -16, -16 }, { 32, 32 }, olc::Colour::BLACK); + + DrawSW.WorldReset(); + DrawSW.WorldOffset({ 124, 64 }); + DrawSW.WorldRotate(fTotal, { 0 ,0 }); + DrawSW.WorldScale({ 2, 2 }); + DrawSW.TexturedRect({ -16, -16 }, { 32, 32 }, + imgTest); + + + + + + DrawSW.WorldReset(); + DrawSW.TexturedTriangle( + { 20,180 }, + mouse.GetPosition(), + { 250, 230 }, + olc::Colour::RED, + olc::Colour::GREEN, + olc::Colour::BLUE, + { 0,0 }, + { 1,0 }, + { 1,1 }, imgTest); + + + + DrawSW.Circle(mouse.GetPosition(), 20, olc::Colour::GREEN); + DrawSW.FilledCircle(mouse.GetPosition(), 10, olc::Colour::CYAN); + + + //// Clear + //TextDraw.SetTarget(textSurface); + // + // + //TextDraw.Clear('-'); + + + //TextDraw.FilledTriangle( + // { 20 / 8, 180 / 8 }, + // mouse.GetPosition() / 8, + // { 250 / 8, 230 / 8 }, + // 'O'); + + + //TextDraw.Line({ 10 / 8,10 / 8 }, mouse.GetPosition() / 8, 'A'); + + + //// Draw TextSurface to Screen + //for (int y = 0; y < textSurface.height(); y++) + //{ + // for (int x = 0; x < textSurface.width(); x++) + // { + // draw.String({ x * 8.0f, y * 8.0f }, std::string(1, textSurface.get(x, y))); + // } + //} + + // Successful frame + return true; + } + +protected: + olc::utils::SWDraw DrawSW; + olc::Image imgTest; + + TextSurface textSurface; + olc::utils::SoftwareRenderer TextDraw; +}; + + +// Main entry point for the application +int main() +{ + // Construct demo application + Example_SoftwareRenderer demo; + + // Create "screen" of 256x240 "pixels" + // with a pixel size of 4x4 actual screen pixels + if (demo.Construct({ 256, 240 }, { 4, 4 })) + { + // Start the application + demo.Start(); + } + + return 0; +} \ No newline at end of file diff --git a/olcPixelGameEngine3.h b/olcPixelGameEngine3.h index 6a4ad22..c4b1408 100644 --- a/olcPixelGameEngine3.h +++ b/olcPixelGameEngine3.h @@ -2379,6 +2379,13 @@ namespace olc bool BoundToGPU() const; bool BoundToCPU() const; + + // SW Renderer IFace + bool set(int x, int y, olc::Pixel p); + olc::Pixel get(int x, int y); + int width(); + int height(); + public: olc::ImageRegion all(); olc::ImageRegion region(const olc::vf2d pos, const olc::vf2d& size); @@ -20668,6 +20675,37 @@ namespace olc return onCPU; } + bool Image::set(int x, int y, olc::Pixel p) + { + if (x >= 0 && y >= 0 && x < dimensions.x && y < dimensions.y) + { + pixels[int(y) * dimensions.x + int(x)] = p; + return true; + } + else + return false; + } + + olc::Pixel Image::get(int x, int y) + { + if (x >= 0 && y >= 0 && x < dimensions.x && y < dimensions.y) + { + return pixels[int(y) * dimensions.x + int(x)]; + } + else + return olc::Colour::BLACK; + } + + int Image::width() + { + return dimensions.x; + } + + int Image::height() + { + return dimensions.y; + } + olc::ImageRegion Image::all() { return olc::ImageRegion(*this); diff --git a/utilities/olcUTIL3_SWRender.h b/utilities/olcUTIL3_SWRender.h new file mode 100644 index 0000000..50c7900 --- /dev/null +++ b/utilities/olcUTIL3_SWRender.h @@ -0,0 +1,769 @@ +/* + OneLoneCoder - Software Renderer v3.0 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + A collection of software rendering routines. Typically these model + that of the basic Draw.xx() routines in PGE3, however they don't + have to be olc::Pixel in olc::Image. As long as your container + and element fulfil the conceptual requirements you can render + anything into anything! + + + License (OLC-3) + ~~~~~~~~~~~~~~~ + + Copyright 2018 - 2026 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. + + Links + ~~~~~ + YouTube: https://www.youtube.com/javidx9 + 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 - 2026 + + Version + ~~~~~~~ + + 3.0 +Initial Release + +*/ + + + +#pragma once + +#include +#include + +namespace olc::utils +{ + template + concept RenderSurface = requires(grid g, int x, int y, T t) + { + { g.set(x, y, t) } -> std::same_as; + { g.get(x, y) } -> std::convertible_to; + { g.width() } -> std::integral; + { g.height() } -> std::integral; + }; + + template + concept lerpable = requires(T a, T b, float t) + { + { a * t } -> std::convertible_to; + { b * t } -> std::convertible_to; + { a + b } -> std::convertible_to; + }; + + template requires lerpable + T flerp(const T& a, const T& b, const float t) + { + return (b * t) + a * (1.0f - t); + } + + template requires RenderSurface + class SoftwareRenderer + { + + + public: + void SetTarget(grid& target) + { + pTarget = ⌖ + vScanlines.resize(pTarget->height()); + } + + void SetWorldTransform(const olc::tf2d& trans) + { + transformAffine = trans; + } + + olc::tf2d& GetWorldTransform() + { + return transformAffine; + } + + void WorldReset() + { + transformAffine = olc::tf2d(); + } + + void WorldScale(const olc::vf2d& vScale) + { + transformAffine.scale(vScale); + } + + void WorldOffset(const olc::vf2d& vOffset) + { + transformAffine.translate(vOffset); + } + + void WorldRotate(const float& fTheta, const olc::vf2d& vPoint) + { + transformAffine.rotate(fTheta, vPoint); + } + + + public: + + + bool Plot(const olc::vi2d& pos, const T& element) + { + const olc::vi2d vTransformedPoints = transformAffine.forward(pos); + return PlotRaw(vTransformedPoints.x, vTransformedPoints.y, element); + } + + bool PlotRaw(const int x, const int y, const T& element) + { + return pTarget->set(x, y, element); + } + + void Clear(const T& element) + { + for (int y = 0; y < pTarget->height(); y++) + { + for (int x = 0; x < pTarget->width(); x++) + { + PlotRaw(x, y, element); + } + } + } + + void Line(const olc::vf2d& p1, const olc::vf2d& p2, const T& element) + { + const auto vTransformedPoints = transformAffine.forward({ p1, p2 }); + swRasterShadedLine( + olc::vi2d(vTransformedPoints[0]), + olc::vi2d(vTransformedPoints[1]), + element, element); + } + + void Line(const olc::vf2d& p1, const olc::vf2d& p2, const T& element1, const T& element2) + { + const auto vTransformedPoints = transformAffine.forward({ p1, p2 }); + swRasterShadedLine( + olc::vi2d(vTransformedPoints[0]), + olc::vi2d(vTransformedPoints[1]), + element1, element2); + } + + void Rect(const olc::vf2d& pos, const olc::vf2d& size, const T& col) + { + Rect(pos, size, col, col, col, col); + } + + void Rect(const olc::vf2d& pos, const olc::vf2d& size, const T& colTL, const T& colTR, const T& colBL, const T& colBR) + { + Line(pos, { pos.x + size.x, pos.y }, colTL, colTR); + Line({ pos.x + size.x, pos.y }, { pos.x + size.x, pos.y + size.y }, colTR, colBR); + Line({ pos.x + size.x, pos.y + size.y }, { pos.x, pos.y + size.y }, colBR, colBL); + Line({ pos.x, pos.y + size.y }, pos, colBL, colTL); + } + + void FilledRect(const olc::vf2d& pos, const olc::vf2d& size, const T& col) + { + FilledRect(pos, size, col, col, col, col); + } + + void FilledRect(const olc::vf2d& pos, const olc::vf2d& size, const T& colTL, const T& colTR, const T& colBL, const T& colBR) + { + const auto vTransformedPoints = transformAffine.forward( + { pos,{ pos.x + size.x, pos.y }, + pos + size,{ pos.x, pos.y + size.y } + }); + + // Check if all one colour + if (colTL == colBL && colTL == colTR && colTL == colBR) + { + // Check if axis aligned + if (vTransformedPoints[0].y == vTransformedPoints[1].y && + vTransformedPoints[1].x == vTransformedPoints[2].x && + vTransformedPoints[2].y == vTransformedPoints[3].y && + vTransformedPoints[3].x == vTransformedPoints[0].x) + { + // Clip to target + olc::vi2d p1 = vTransformedPoints[0].max({ 0,0 }); + olc::vi2d p2 = vTransformedPoints[2].min(pTarget->Size()); + + // Draw filled rectangle + for (int32_t y = p1.y; y < p2.y; y++) + for (int32_t x = p1.x; x < p2.x; x++) + PlotRaw(x, y, colTL); + + // Exit early + return; + } + } + + // Fallback to general case rasteriser, where we split into two triangles + swRasterShadedTriangle( + olc::vi2d(vTransformedPoints[0]), + olc::vi2d(vTransformedPoints[1]), + olc::vi2d(vTransformedPoints[2]), + colTL, colTR, colBR); + swRasterShadedTriangle( + olc::vi2d(vTransformedPoints[0]), + olc::vi2d(vTransformedPoints[2]), + olc::vi2d(vTransformedPoints[3]), + colTL, colBR, colBL); + } + + void TexturedRect( + const olc::vf2d& pos, + const olc::vf2d& size, + olc::Image& texture, + const olc::vf2d& t1 = { 0,0 }, + const olc::vf2d& t2 = { 1,0 }, + const olc::vf2d& t3 = { 1,1 }, + const olc::vf2d& t4 = { 0,1 }, + const T& colTL = olc::Colour::WHITE, + const T& colTR = olc::Colour::WHITE, + const T& colBL = olc::Colour::WHITE, + const T& colBR = olc::Colour::WHITE) + { + const auto vTransformedPoints = transformAffine.forward( + { pos,{ pos.x + size.x, pos.y }, + pos + size,{ pos.x, pos.y + size.y } + }); + + swRasterTexturedTriangle( + olc::vi2d(vTransformedPoints[0]), + olc::vi2d(vTransformedPoints[1]), + olc::vi2d(vTransformedPoints[2]), + colTL, colTR, colBR, + t1, t2, t3, texture); + swRasterTexturedTriangle( + olc::vi2d(vTransformedPoints[0]), + olc::vi2d(vTransformedPoints[2]), + olc::vi2d(vTransformedPoints[3]), + colTL, colBR, colBL, + t1, t3, t4, texture); + } + + + void Triangle(const olc::vf2d& p1, const olc::vf2d& p2, const olc::vf2d& p3, const T& col) + { + Triangle(p1, p2, p3, col, col, col); + } + + void Triangle(const olc::vf2d& p1, const olc::vf2d& p2, const olc::vf2d& p3, const T& c1, const T& c2, const T& c3) + { + Line(p1, p2, c1, c2); + Line(p2, p3, c2, c3); + Line(p3, p1, c3, c1); + } + + void FilledTriangle(const olc::vf2d& p1, const olc::vf2d& p2, const olc::vf2d& p3, const T& col) + { + FilledTriangle(p1, p2, p3, col, col, col); + } + + void FilledTriangle(const olc::vf2d& p1, const olc::vf2d& p2, const olc::vf2d& p3, const T& c1, const T& c2, const T& c3) + { + const auto vTransformedPoints = transformAffine.forward({ p1, p2, p3 }); + swRasterShadedTriangle( + olc::vi2d(vTransformedPoints[0]), + olc::vi2d(vTransformedPoints[1]), + olc::vi2d(vTransformedPoints[2]), + c1, c2, c3); + } + + void TexturedTriangle(const olc::vf2d& p1, const olc::vf2d& p2, const olc::vf2d& p3, const T& c1, const T& c2, const T& c3, const olc::vf2d& t1, const olc::vf2d& t2, const olc::vf2d& t3, olc::Image& texture) + { + const auto vTransformedPoints = transformAffine.forward({ p1, p2, p3 }); + swRasterTexturedTriangle( + olc::vi2d(vTransformedPoints[0]), + olc::vi2d(vTransformedPoints[1]), + olc::vi2d(vTransformedPoints[2]), + c1, c2, c3, + t1, t2, t3, + texture); + } + + + void Circle(const olc::vf2d& p, float radius, const T& element, uint8_t mask = 0xFF) + { + const olc::vi2d vTransformedPoint = transformAffine.forwardRound(p); + const int fTransformedRadius = int(transformAffine.scale().x * radius); + + if (fTransformedRadius < 0 + || vTransformedPoint.x < -fTransformedRadius + || vTransformedPoint.y < -fTransformedRadius + || vTransformedPoint.x - pTarget->width() > fTransformedRadius + || vTransformedPoint.y - pTarget->height() > fTransformedRadius) + return; + + if (fTransformedRadius > 0) + { + int x0 = 0; + int y0 = int(fTransformedRadius); + int d = 3 - 2 * y0; + + while (y0 >= x0) // only formulate 1/8 of circle + { + // Draw even octants + if (mask & 0x01) PlotRaw(vTransformedPoint.x + x0, vTransformedPoint.y - y0, element);// Q6 - upper right right + if (mask & 0x04) PlotRaw(vTransformedPoint.x + y0, vTransformedPoint.y + x0, element);// Q4 - lower lower right + if (mask & 0x10) PlotRaw(vTransformedPoint.x - x0, vTransformedPoint.y + y0, element);// Q2 - lower left left + if (mask & 0x40) PlotRaw(vTransformedPoint.x - y0, vTransformedPoint.y - x0, element);// Q0 - upper upper left + if (x0 != 0 && x0 != y0) + { + if (mask & 0x02) PlotRaw(vTransformedPoint.x + y0, vTransformedPoint.y - x0, element);// Q7 - upper upper right + if (mask & 0x08) PlotRaw(vTransformedPoint.x + x0, vTransformedPoint.y + y0, element);// Q5 - lower right right + if (mask & 0x20) PlotRaw(vTransformedPoint.x - y0, vTransformedPoint.y + x0, element);// Q3 - lower lower left + if (mask & 0x80) PlotRaw(vTransformedPoint.x - x0, vTransformedPoint.y - y0, element);// Q1 - upper left left + } + + if (d < 0) + d += 4 * x0++ + 6; + else + d += 4 * (x0++ - y0--) + 10; + } + } + else + PlotRaw(vTransformedPoint.x, vTransformedPoint.y, element); + } + + + void FilledCircle(const olc::vf2d& p, float radius, const T& element) + { + const olc::vi2d vTransformedPoint = transformAffine.forwardRound(p); + const int fTransformedRadius = int(transformAffine.scale().x * radius); + + if (fTransformedRadius < 0 + || vTransformedPoint.x < -fTransformedRadius + || vTransformedPoint.y < -fTransformedRadius + || vTransformedPoint.x - pTarget->width() > fTransformedRadius + || vTransformedPoint.y - pTarget->height() > fTransformedRadius) + return; + + if (fTransformedRadius > 0) + { + int x0 = 0; + int y0 = int(fTransformedRadius); + int d = 3 - 2 * y0; + + auto drawline = [&](int sx, int ex, int y) + { + for (int x = sx; x <= ex; x++) + PlotRaw(x, y, element); + }; + + while (y0 >= x0) + { + drawline(vTransformedPoint.x - y0, vTransformedPoint.x + y0, vTransformedPoint.y - x0); + if (x0 > 0) drawline(vTransformedPoint.x - y0, vTransformedPoint.x + y0, vTransformedPoint.y + x0); + + if (d < 0) + d += 4 * x0++ + 6; + else + { + if (x0 != y0) + { + drawline(vTransformedPoint.x - x0, vTransformedPoint.x + x0, vTransformedPoint.y - y0); + drawline(vTransformedPoint.x - x0, vTransformedPoint.x + x0, vTransformedPoint.y + y0); + } + d += 4 * (x0++ - y0--) + 10; + } + } + } + else + PlotRaw(vTransformedPoint.x, vTransformedPoint.y, element); + } + + + + protected: + // Clips a line to a rectangular region, returns true if line is visible. + // The returned weights correspond to distance along the line from v0 to v1 + bool swClipWeightedLine(olc::vf2d& v0, olc::vf2d& v1, const olc::vf2d& vMin, const olc::vf2d& vMax, float& w0, float& w1) + { + // Liang-Barsky line clipping algorithm adapted for weighted lines + // https://en.wikipedia.org/wiki/Liang%E2%80%93Barsky_algorithm + + olc::vf2d diff = v1 - v0; + + float p[4] = { -diff.x, diff.x, -diff.y, diff.y }; + + float q[4] = + { + v0.x - vMin.x, + vMax.x - v0.x, + v0.y - vMin.y, + vMax.y - v0.y + }; + + // Weights are ideal to start, we'll contact them as we clip + w0 = 0.0f; + w1 = 1.0f; + + for (int i = 0; i < 4; i++) + { + if (p[i] == 0.0f) + { + if (q[i] < 0.0f) + return false; // Line is parallel and outside the clipping boundary + } + else + { + float t = float(q[i]) / float(p[i]); + if (p[i] < 0.0f) + { + if (t > w1) + return false; // Line is outside the clipping boundary + else if (t > w0) + w0 = t; + } + else + { + if (t < w0) + return false; // Line is outside the clipping boundary + else if (t < w1) + w1 = t; + } + } + } + + if (w1 < w0) + return false; // Line is outside the clipping boundary + + // Return new line segment ends + olc::vf2d v = v0; + v0 = v + (diff * w0); + v1 = v + (diff * w1); + + // Line has visible pixels inside clipping boundary + return true; + } + + // Rasterises a shaded line in integer space + void swRasterShadedLine(const olc::vi2d& v1, const olc::vi2d& v2, const T& c1, const T& c2) + { + // Lambda to draw a pixel gated by a pattern bit + uint32_t pattern = 0xFFFFFFFF; + auto rol = [&](void) + { + pattern = (pattern << 1) | (pattern >> 31); + return pattern & 1; + }; + + // Clip line to draw target + olc::vf2d clipped_p1 = v1; + olc::vf2d clipped_p2 = v2; + + float w0 = 0, w1 = 1; + if (!swClipWeightedLine(clipped_p1, clipped_p2, { 0,0 }, olc::vf2d(float(pTarget->width()), float(pTarget->height())), w0, w1)) + return; + + // Move to integer space + olc::vi2d ip1 = clipped_p1; + olc::vi2d ip2 = clipped_p2; + olc::vi2d pixel; + + // Calculate deltas + int dx = ip2.x - ip1.x; + int dy = ip2.y - ip1.y; + int absDx = std::abs(dx); + int absDy = std::abs(dy); + + // Determine dominant axis + bool xMajor = absDx >= absDy; + int steps = xMajor ? absDx : absDy; + + // Handle degenerate case (single pixel) + if (steps == 0) + { + PlotRaw(ip1.x, ip1.y, c1); + return; + } + + // Calculate step increments + float xStep = float(dx) / float(steps); + float yStep = float(dy) / float(steps); + float colorStep = 1.0f / float(steps) * (w1 - w0); + + T cStart = flerp(c1, c2, w0); + T cEnd = flerp(c1, c2, w1); + + + + // Starting position and color interpolation parameter + float x = float(ip1.x); + float y = float(ip1.y); + float t = 0.0f; + + // Draw line pixel by pixel + for (int i = 0; i <= steps; i++) + { + // Interpolate color + //olc::Pixel col = olc::PixelLerp(c1, c2, t); + T col = flerp(cStart, cEnd, t); + + // Plot pixel + if (rol()) + PlotRaw((int)std::round(x), (int)std::round(y), col); + + // Step to next pixel + x += xStep; + y += yStep; + t += colorStep; + } + } + + void swRasterShadedTriangle(const olc::vi2d& v1, const olc::vi2d& v2, const olc::vi2d& v3, const T& c1, const T& c2, const T& c3) + { + // Calculate barycentric outline + auto [y_min, y_max] = swBaryFillTriangle(v1, v2, v3); + + // Now draw the scanlines + for (int32_t y = y_min; y < y_max; y++) + { + const auto& scanline = vScanlines[y]; + + int32_t xStart = scanline.nMin; + int32_t xEnd = scanline.nMax; + + int32_t x_min = std::max(0, xStart); + int32_t x_max = std::min(xEnd, pTarget->width()); + + float fSpan = float(xEnd - xStart); + float fSpanStep = fSpan > 0.0f ? 1.0f / fSpan : 0.0f; + + float b0_step = fSpanStep * (scanline.fBaryMax[0] - scanline.fBaryMin[0]); + float b1_step = fSpanStep * (scanline.fBaryMax[1] - scanline.fBaryMin[1]); + float b2_step = fSpanStep * (scanline.fBaryMax[2] - scanline.fBaryMin[2]); + + float b0 = scanline.fBaryMin[0]; + float b1 = scanline.fBaryMin[1]; + float b2 = scanline.fBaryMin[2]; + + if (xStart < 0) + { + b0 = scanline.fBaryMin[0] + (-xStart * b0_step); + b1 = scanline.fBaryMin[1] + (-xStart * b1_step); + b2 = scanline.fBaryMin[2] + (-xStart * b2_step); + } + + for (int32_t x = x_min; x < x_max; x++) + { + T col = c1 * b0 + c2 * b1 + c3 * b2; + PlotRaw(x, y, col); + b0 += b0_step; + b1 += b1_step; + b2 += b2_step; + } + } + } + + void swRasterTexturedTriangle(const olc::vi2d& v1, const olc::vi2d& v2, const olc::vi2d& v3, const T& c1, const T& c2, const T& c3, const olc::vf2d& t1, const olc::vf2d& t2, const olc::vf2d& t3, grid& texture) + { + auto [y_min, y_max] = swBaryFillTriangle(v1, v2, v3); + + // Now draw the scanlines + for (int32_t y = y_min; y < y_max; y++) + { + const auto& scanline = vScanlines[y]; + + int32_t xStart = scanline.nMin; + int32_t xEnd = scanline.nMax; + + int32_t x_min = std::max(0, xStart); + int32_t x_max = std::min(xEnd, pTarget->width()); + + float fSpan = float(xEnd - xStart); + float fSpanStep = fSpan > 0.0f ? 1.0f / fSpan : 0.0f; + + float b0_step = fSpanStep * (scanline.fBaryMax[0] - scanline.fBaryMin[0]); + float b1_step = fSpanStep * (scanline.fBaryMax[1] - scanline.fBaryMin[1]); + float b2_step = fSpanStep * (scanline.fBaryMax[2] - scanline.fBaryMin[2]); + + float b0 = scanline.fBaryMin[0]; + float b1 = scanline.fBaryMin[1]; + float b2 = scanline.fBaryMin[2]; + + if (xStart < 0) + { + b0 = scanline.fBaryMin[0] + (-xStart * b0_step); + b1 = scanline.fBaryMin[1] + (-xStart * b1_step); + b2 = scanline.fBaryMin[2] + (-xStart * b2_step); + } + + float w = float(texture.width()); + float h = float(texture.height()); + + for (int32_t x = x_min; x < x_max; x++) + { + T col = c1 * b0 + c2 * b1 + c3 * b2; + + olc::vf2d uv = olc::vf2d( + b0 * t1.x + b1 * t2.x + b2 * t3.x, + b0 * t1.y + b1 * t2.y + b2 * t3.y); + + PlotRaw(x, y, texture.get(int(uv.x * w), int(uv.y * h))); + b0 += b0_step; + b1 += b1_step; + b2 += b2_step; + } + } + + return; + } + + + protected: + grid* pTarget = nullptr; + olc::tf2d transformAffine; + + struct Scanline + { + int32_t nMin = std::numeric_limits::max(); + int32_t nMax = std::numeric_limits::min(); + std::array fBaryMin; + std::array fBaryMax; + }; + std::vector vScanlines; + + // Fills scanline buffer with visible triangle extents and barycentric coordinates. + // Returns vertical, visible extents of triangle scanlines + std::pair swBaryFillTriangle(const olc::vi2d& v1, const olc::vi2d& v2, const olc::vi2d& v3) + { + // Get height of triangle in whole pixels + int32_t nMinY = std::min({ v1.y, v2.y, v3.y }); + int32_t nMaxY = std::max({ v1.y, v2.y, v3.y }); + int32_t nHeight = nMaxY - nMinY; + + if (nHeight <= 0) + return { 0, 0 }; // Degenerate triangle + + // Scanline buffer is already allocated to be the max vertical size + // of the draw target. Obviously it only represents visible scanlines + // that are to be filled for the current triangle. + + // Get visible height of triangle + int32_t y_min = std::max(0, nMinY); + int32_t y_max = std::min(nMaxY, pTarget->height()); + + // Zero out scanline buffer (by resetting min and max values) + for (int32_t y = y_min; y < y_max; y++) + { + vScanlines[y].nMin = std::numeric_limits::max(); + vScanlines[y].nMax = std::numeric_limits::min(); + } + + // This function scans an edge of the triangle, updating + // the scanline buffer with min/max extents and barycentric coords. + // It returns the number of scanlines updated. + auto scanEdge = [&](olc::vi2d p0, olc::vi2d p1, int id1, int id2) -> size_t + { + if (p0.y == p1.y) + return 0; + + // Ensure p0.y < p1.y + bool swapped = false; + if (p0.y > p1.y) + { + std::swap(p0, p1); + swapped = true; + } + + // Cache edge step deltas + int dy = p1.y - p0.y; + float dx_step = (p1.x - p0.x) / float(dy); + float dy_step = 1.0f / float(dy); + float x = float(p0.x); + + // Rasterise edge - if pixel lies on visible scanline then + // update the scanline bounds and barycentric coords + size_t nScanline = 0; + for (int y = p0.y; y <= p1.y; y++) + { + // If this pixel row is visible + if (y >= 0 && y < vScanlines.size()) + { + int ix = int(std::round(x)); + + // interpolation along edge + // Note: We may need to do this differently when clipping + float t = (y - p0.y) * dy_step; + + std::array bary = { 0.0f, 0.0f, 0.0f }; + + // Set barycentric coords depending on edge direction + if (swapped) + { + bary[id1] = t; + bary[id2] = 1.0f - t; + } + else + { + bary[id1] = 1.0f - t; + bary[id2] = t; + } + + // Update scanline extents and barycentric coords + if (ix < vScanlines[y].nMin) + { + vScanlines[y].nMin = ix; + vScanlines[y].fBaryMin = bary; + } + + if (ix > vScanlines[y].nMax) + { + vScanlines[y].nMax = ix; + vScanlines[y].fBaryMax = bary; + } + + nScanline++; + } + + x += dx_step; + } + + return nScanline; + }; + + // Rasterise triangle edges into scanline buffer + scanEdge(v1, v2, 0, 1); + scanEdge(v1, v3, 0, 2); + scanEdge(v2, v3, 1, 2); + + return { y_min, y_max }; + } + + }; + + + typedef SoftwareRenderer SWDraw; +} \ No newline at end of file