From 6562ecc9367f3edf973af58d55009fef75f1ff87 Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Wed, 2 Jan 2019 07:28:34 +0000 Subject: [PATCH 01/12] Update LICENCE.md --- LICENCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE.md b/LICENCE.md index fc18783..0eefa43 100644 --- a/LICENCE.md +++ b/LICENCE.md @@ -1,6 +1,6 @@ # License (OLC-3) -Copyright 2018 OneLoneCoder.com +Copyright 2018-2019 OneLoneCoder.com Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions From 45a7bfe32a30910d7e59e8ea21c2e6fd77c19bea Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Thu, 3 Jan 2019 20:10:24 +0000 Subject: [PATCH 02/12] Version 1.12 Added Numpad Keys, Fixed ResourcePack code on Linux Updated Sound PGEX to be compilable, but not functional on linux --- olcPGEX_Sound.h | 192 +++++++++++++++++++++++++++++++++++++++---- olcPixelGameEngine.h | 52 ++++++++---- 2 files changed, 214 insertions(+), 30 deletions(-) diff --git a/olcPGEX_Sound.h b/olcPGEX_Sound.h index 059696d..c6f3b44 100644 --- a/olcPGEX_Sound.h +++ b/olcPGEX_Sound.h @@ -69,6 +69,16 @@ #undef min #undef max +typedef struct { + unsigned short wFormatTag; + unsigned short nChannels; + unsigned long nSamplesPerSec; + unsigned long nAvgBytesPerSec; + unsigned short nBlockAlign; + unsigned short wBitsPerSample; + unsigned short cbSize; +} OLC_WAVEFORMATEX; + namespace olc { // Container class for Advanced 2D Drawing functions @@ -84,7 +94,7 @@ namespace olc olc::rcode LoadFromFile(std::string sWavFile, olc::ResourcePack *pack = nullptr); public: - WAVEFORMATEX wavHeader; + OLC_WAVEFORMATEX wavHeader; float *fSample = nullptr; long nSamples = 0; int nChannels = 0; @@ -115,10 +125,10 @@ namespace olc static void StopAll(); static float GetMixerOutput(int nChannel, float fGlobalTime, float fTimeStep); -#ifdef WIN32 + private: - static void CALLBACK waveOutProc(HWAVEOUT hWaveOut, UINT uMsg, DWORD dwParam1, DWORD dwParam2); - static void AudioThread(); +#ifdef WIN32 // Windows specific sound management + static void CALLBACK waveOutProc(HWAVEOUT hWaveOut, UINT uMsg, DWORD dwParam1, DWORD dwParam2); static unsigned int m_nSampleRate; static unsigned int m_nChannels; static unsigned int m_nBlockCount; @@ -126,16 +136,19 @@ namespace olc static unsigned int m_nBlockCurrent; static short* m_pBlockMemory; static WAVEHDR *m_pWaveHeaders; - static HWAVEOUT m_hwDevice; - static std::thread m_AudioThread; - static std::atomic m_bAudioThreadActive; + static HWAVEOUT m_hwDevice; static std::atomic m_nBlockFree; static std::condition_variable m_cvBlockNotZero; static std::mutex m_muxBlockNotZero; +#endif + + static void AudioThread(); + static std::thread m_AudioThread; + static std::atomic m_bAudioThreadActive; static std::atomic m_fGlobalTime; static std::function funcUserSynth; static std::function funcUserFilter; -#endif + }; } @@ -143,14 +156,11 @@ namespace olc #ifdef WIN32 #pragma comment(lib, "winmm.lib") + namespace olc { SOUND::AudioSample::AudioSample() - { - - - - } + { } SOUND::AudioSample::AudioSample(std::string sWavFile, olc::ResourcePack *pack) { @@ -506,8 +516,162 @@ namespace olc std::function SOUND::funcUserSynth = nullptr; std::function SOUND::funcUserFilter = nullptr; } + +#else // Non Windows +namespace olc +{ + SOUND::AudioSample::AudioSample() + {} + + SOUND::AudioSample::AudioSample(std::string sWavFile, olc::ResourcePack *pack) + { + LoadFromFile(sWavFile, pack); + } + + olc::rcode SOUND::AudioSample::LoadFromFile(std::string sWavFile, olc::ResourcePack *pack) + { + return olc::OK; + } + + bool SOUND::InitialiseAudio(unsigned int nSampleRate, unsigned int nChannels, unsigned int nBlocks, unsigned int nBlockSamples) + { + return true; + } + + // Stop and clean up audio system + bool SOUND::DestroyAudio() + { + return false; + } + + + // Audio thread. This loop responds to requests from the soundcard to fill 'blocks' + // with audio data. If no requests are available it goes dormant until the sound + // card is ready for more data. The block is fille by the "user" in some manner + // and then issued to the soundcard. + void SOUND::AudioThread() + { + + } + + // This vector holds all loaded sound samples in memory + std::vector vecAudioSamples; + + // This structure represents a sound that is currently playing. It only + // holds the sound ID and where this instance of it is up to for its + // current playback + + void SOUND::SetUserSynthFunction(std::function func) + { + funcUserSynth = func; + } + + void SOUND::SetUserFilterFunction(std::function func) + { + funcUserFilter = func; + } + + // Load a 16-bit WAVE file @ 44100Hz ONLY into memory. A sample ID + // number is returned if successful, otherwise -1 + unsigned int SOUND::LoadAudioSample(std::string sWavFile, olc::ResourcePack *pack) + { + olc::SOUND::AudioSample a(sWavFile, pack); + if (a.bSampleValid) + { + vecAudioSamples.push_back(a); + return vecAudioSamples.size(); + } + else + return -1; + } + + // Add sample 'id' to the mixers sounds to play list + void SOUND::PlaySample(int id, bool bLoop) + { + olc::SOUND::sCurrentlyPlayingSample a; + a.nAudioSampleID = id; + a.nSamplePosition = 0; + a.bFinished = false; + a.bFlagForStop = false; + a.bLoop = bLoop; + SOUND::listActiveSamples.push_back(a); + } + + void SOUND::StopSample(int id) + { + // Find first occurence of sample id + auto s = std::find_if(listActiveSamples.begin(), listActiveSamples.end(), [&](const olc::SOUND::sCurrentlyPlayingSample &s) { return s.nAudioSampleID == id; }); + if (s != listActiveSamples.end()) + s->bFlagForStop = true; + } + + void SOUND::StopAll() + { + for (auto &s : listActiveSamples) + { + s.bFlagForStop = true; + } + } + + float SOUND::GetMixerOutput(int nChannel, float fGlobalTime, float fTimeStep) + { + // Accumulate sample for this channel + float fMixerSample = 0.0f; + + for (auto &s : listActiveSamples) + { + if (m_bAudioThreadActive) + { + if (s.bFlagForStop) + { + s.bLoop = false; + s.bFinished = true; + } + else + { + // Calculate sample position + s.nSamplePosition += (long)((float)vecAudioSamples[s.nAudioSampleID - 1].wavHeader.nSamplesPerSec * fTimeStep); + + // If sample position is valid add to the mix + if (s.nSamplePosition < vecAudioSamples[s.nAudioSampleID - 1].nSamples) + fMixerSample += vecAudioSamples[s.nAudioSampleID - 1].fSample[(s.nSamplePosition * vecAudioSamples[s.nAudioSampleID - 1].nChannels) + nChannel]; + else + { + if (s.bLoop) + { + s.nSamplePosition = 0; + } + else + s.bFinished = true; // Else sound has completed + } + } + } + else + return 0.0f; + } + + // If sounds have completed then remove them + listActiveSamples.remove_if([](const sCurrentlyPlayingSample &s) {return s.bFinished; }); + + // The users application might be generating sound, so grab that if it exists + if (funcUserSynth != nullptr) + fMixerSample += funcUserSynth(nChannel, fGlobalTime, fTimeStep); + + // Return the sample via an optional user override to filter the sound + if (funcUserFilter != nullptr) + return funcUserFilter(nChannel, fGlobalTime, fMixerSample); + else + return fMixerSample; + } + + std::thread SOUND::m_AudioThread; + std::atomic SOUND::m_bAudioThreadActive{ false }; + std::atomic SOUND::m_fGlobalTime{ 0.0f }; + std::list SOUND::listActiveSamples; + std::function SOUND::funcUserSynth = nullptr; + std::function SOUND::funcUserFilter = nullptr; +} #endif -// Currently no Linux implementation so just go blank :( #endif \ No newline at end of file diff --git a/olcPixelGameEngine.h b/olcPixelGameEngine.h index af73f99..aa97394 100644 --- a/olcPixelGameEngine.h +++ b/olcPixelGameEngine.h @@ -2,11 +2,9 @@ olcPixelGameEngine.h +-------------------------------------------------------------+ - | OneLoneCoder Pixel Game Engine v1.11 | + | OneLoneCoder Pixel Game Engine v1.12 | | "Like the command prompt console one, but not..." - javidx9 | +-------------------------------------------------------------+ - - The Original & Best... :P What is this? ~~~~~~~~~~~~~ @@ -90,6 +88,7 @@ Twitch: https://www.twitch.tv/javidx9 GitHub: https://www.github.com/onelonecoder Homepage: https://www.onelonecoder.com + Patreon: https://www.patreon.com/javidx9 Relevant Videos ~~~~~~~~~~~~~~~ @@ -121,17 +120,20 @@ ~~~~~~ I'd like to extend thanks to Eremiell, slavka, gurkanctn, Phantim, JackOJC, KrossX, Huhlig, Dragoneye, Appa, JustinRichardsMusic, SliceNDice - & MagetzUb for advice, ideas and testing, and I'd like to extend - my appreciation to the 14K YouTube followers and 1K Discord server + Ralakus, Gorbit99, raoul & MagetzUb for advice, ideas and testing, and I'd like + to extend my appreciation to the 23K YouTube followers and 1.5K Discord server members who give me the motivation to keep going with all this :D Special thanks to those who bring gifts! GnarGnarHead.......Domina Gorbit99...........Bastion + Special thanks to my Patreons too - I wont name you on here, but I've + certainly enjoyed my tea and flapjacks :D + Author ~~~~~~ - David Barr, aka javidx9, ©OneLoneCoder 2018 + David Barr, aka javidx9, ©OneLoneCoder 2018, 2019 */ ////////////////////////////////////////////////////////////////////////////////////////// @@ -279,7 +281,8 @@ namespace olc // All OneLoneCoder stuff will now exist in the "olc" namespace ResourcePack(); ~ResourcePack(); struct sEntry : public std::streambuf { - uint32_t nID, nFileOffset, nFileSize; uint8_t* data; void _config() { this->setg((char*)data, (char*)data, (char*)(data + nFileSize)); }}; + uint32_t nID, nFileOffset, nFileSize; uint8_t* data; void _config() { this->setg((char*)data, (char*)data, (char*)(data + nFileSize)); } + }; public: olc::rcode AddToPack(std::string sFile); @@ -346,7 +349,9 @@ namespace olc // All OneLoneCoder stuff will now exist in the "olc" namespace F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, UP, DOWN, LEFT, RIGHT, SPACE, TAB, SHIFT, CTRL, INS, DEL, HOME, END, PGUP, PGDN, - BACK, ESCAPE, ENTER, PAUSE, SCROLL, + BACK, ESCAPE, RETURN, ENTER, PAUSE, SCROLL, + NP0, NP1, NP2, NP3, NP4, NP5, NP6, NP7, NP8, NP9, + NP_MUL, NP_DIV, NP_ADD, NP_SUB, NP_DECIMAL, }; @@ -402,6 +407,7 @@ namespace olc // All OneLoneCoder stuff will now exist in the "olc" namespace // olc::Pixel::MASK = Transparent if alpha is < 255 // olc::Pixel::ALPHA = Full transparency void SetPixelMode(Pixel::Mode m); + Pixel::Mode GetPixelMode(); // Use a custom blend function void SetPixelMode(std::function pixelMode); // Change the blend factor form between 0.0f to 1.0f; @@ -647,7 +653,8 @@ namespace olc } else { - std::istream is(&(pack->GetStreamBuffer(sImageFile))); + auto streamBuffer = pack->GetStreamBuffer(sImageFile); + std::istream is(&streamBuffer); ReadData(is); } @@ -852,10 +859,10 @@ namespace olc // Create entry sEntry e; e.data = nullptr; - e.nFileSize = p; + e.nFileSize = (uint32_t)p; // Read file into memory - e.data = new uint8_t[e.nFileSize]; + e.data = new uint8_t[(uint32_t)e.nFileSize]; ifs.read((char*)e.data, e.nFileSize); ifs.close(); @@ -886,7 +893,7 @@ namespace olc std::streampos offset = ofs.tellp(); for (auto &e : mapFiles) { - e.second.nFileOffset = offset; + e.second.nFileOffset = (uint32_t)offset; ofs.write((char*)e.second.data, e.second.nFileSize); offset += e.second.nFileSize; } @@ -936,12 +943,10 @@ namespace olc // 2) Read Data for (auto &e : mapFiles) { - e.second.data = new uint8_t[e.second.nFileSize]; + e.second.data = new uint8_t[(uint32_t)e.second.nFileSize]; ifs.seekg(e.second.nFileOffset); ifs.read((char*)e.second.data, e.second.nFileSize); - //e.second.setg e.second._config(); - //e.second.pubsetbuf((char*)e.second.data, e.second.nFileSize); } ifs.close(); @@ -1542,6 +1547,11 @@ namespace olc nPixelMode = m; } + Pixel::Mode PixelGameEngine::GetPixelMode() + { + return nPixelMode; + } + void PixelGameEngine::SetPixelMode(std::function pixelMode) { funcPixelMode = pixelMode; @@ -1884,6 +1894,7 @@ namespace olc mapKeys[VK_F9] = Key::F9; mapKeys[VK_F10] = Key::F10; mapKeys[VK_F11] = Key::F11; mapKeys[VK_F12] = Key::F12; mapKeys[VK_DOWN] = Key::DOWN; mapKeys[VK_LEFT] = Key::LEFT; mapKeys[VK_RIGHT] = Key::RIGHT; mapKeys[VK_UP] = Key::UP; + mapKeys[VK_RETURN] = Key::ENTER; //mapKeys[VK_RETURN] = Key::RETURN; mapKeys[VK_BACK] = Key::BACK; mapKeys[VK_ESCAPE] = Key::ESCAPE; mapKeys[VK_RETURN] = Key::ENTER; mapKeys[VK_PAUSE] = Key::PAUSE; mapKeys[VK_SCROLL] = Key::SCROLL; mapKeys[VK_TAB] = Key::TAB; mapKeys[VK_DELETE] = Key::DEL; mapKeys[VK_HOME] = Key::HOME; @@ -1894,6 +1905,10 @@ namespace olc mapKeys[0x30] = Key::K0; mapKeys[0x31] = Key::K1; mapKeys[0x32] = Key::K2; mapKeys[0x33] = Key::K3; mapKeys[0x34] = Key::K4; mapKeys[0x35] = Key::K5; mapKeys[0x36] = Key::K6; mapKeys[0x37] = Key::K7; mapKeys[0x38] = Key::K8; mapKeys[0x39] = Key::K9; + mapKeys[VK_NUMPAD0] = Key::NP0; mapKeys[VK_NUMPAD1] = Key::NP1; mapKeys[VK_NUMPAD2] = Key::NP2; mapKeys[VK_NUMPAD3] = Key::NP3; mapKeys[VK_NUMPAD4] = Key::NP4; + mapKeys[VK_NUMPAD5] = Key::NP5; mapKeys[VK_NUMPAD6] = Key::NP6; mapKeys[VK_NUMPAD7] = Key::NP7; mapKeys[VK_NUMPAD8] = Key::NP8; mapKeys[VK_NUMPAD9] = Key::NP9; + mapKeys[VK_MULTIPLY] = Key::NP_MUL; mapKeys[VK_ADD] = Key::NP_ADD; mapKeys[VK_DIVIDE] = Key::NP_DIV; mapKeys[VK_SUBTRACT] = Key::NP_SUB; mapKeys[VK_DECIMAL] = Key::NP_DECIMAL; + return olc_hWnd; } @@ -1995,6 +2010,7 @@ namespace olc mapKeys[XK_F9] = Key::F9; mapKeys[XK_F10] = Key::F10; mapKeys[XK_F11] = Key::F11; mapKeys[XK_F12] = Key::F12; mapKeys[XK_Down] = Key::DOWN; mapKeys[XK_Left] = Key::LEFT; mapKeys[XK_Right] = Key::RIGHT; mapKeys[XK_Up] = Key::UP; + mapKeys[XK_KP_Enter] = Key::ENTER; mapKeys[XK_Return] = Key::ENTER; mapKeys[XK_BackSpace] = Key::BACK; mapKeys[XK_Escape] = Key::ESCAPE; mapKeys[XK_Linefeed] = Key::ENTER; mapKeys[XK_Pause] = Key::PAUSE; mapKeys[XK_Scroll_Lock] = Key::SCROLL; mapKeys[XK_Tab] = Key::TAB; mapKeys[XK_Delete] = Key::DEL; mapKeys[XK_Home] = Key::HOME; @@ -2005,6 +2021,10 @@ namespace olc mapKeys[XK_0] = Key::K0; mapKeys[XK_1] = Key::K1; mapKeys[XK_2] = Key::K2; mapKeys[XK_3] = Key::K3; mapKeys[XK_4] = Key::K4; mapKeys[XK_5] = Key::K5; mapKeys[XK_6] = Key::K6; mapKeys[XK_7] = Key::K7; mapKeys[XK_8] = Key::K8; mapKeys[XK_9] = Key::K9; + mapKeys[XK_KP_0] = Key::NP0; mapKeys[XK_KP_1] = Key::NP1; mapKeys[XK_KP_2] = Key::NP2; mapKeys[XK_KP_3] = Key::NP3; mapKeys[XK_KP_4] = Key::NP4; + mapKeys[XK_KP_5] = Key::NP5; mapKeys[XK_KP_6] = Key::NP6; mapKeys[XK_KP_7] = Key::NP7; mapKeys[XK_KP_8] = Key::NP8; mapKeys[XK_KP_9] = Key::NP9; + mapKeys[XK_KP_Multiply] = Key::NP_MUL; mapKeys[XK_KP_Add] = Key::NP_ADD; mapKeys[XK_KP_Divide] = Key::NP_DIV; mapKeys[XK_KP_Subtract] = Key::NP_SUB; mapKeys[XK_KP_Decimal] = Key::NP_DECIMAL; + return olc_Display; } @@ -2044,4 +2064,4 @@ namespace olc //============================================================= } -#endif +#endif \ No newline at end of file From 5e1f9c4f3a4ae50fb25610b2e3a0dad267e61b03 Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Mon, 7 Jan 2019 21:45:43 +0000 Subject: [PATCH 03/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c63520..5d4e597 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Please see https://github.com/OneLoneCoder/olcPixelGameEngine/wiki # License (OLC-3) -Copyright 2018 OneLoneCoder.com +Copyright 2018, 2019 OneLoneCoder.com Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions From 9d1fb5949c523af32f8ffa562e6378c1a851ee6b Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sat, 19 Jan 2019 22:55:56 +0000 Subject: [PATCH 04/12] Initial version of PGEX_Graphics3D --- olcPGEX_Graphics3D.h | 1173 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1173 insertions(+) create mode 100644 olcPGEX_Graphics3D.h diff --git a/olcPGEX_Graphics3D.h b/olcPGEX_Graphics3D.h new file mode 100644 index 0000000..d3e217a --- /dev/null +++ b/olcPGEX_Graphics3D.h @@ -0,0 +1,1173 @@ +/* + olcPGEX_Graphics3D.h + + +-------------------------------------------------------------+ + | OneLoneCoder Pixel Game Engine Extension | + | 3D Rendering - v0.1 | + +-------------------------------------------------------------+ + + What is this? + ~~~~~~~~~~~~~ + This is an extension to the olcPixelGameEngine, which provides + support for software rendering 3D graphics. + + NOTE!!! This file is under development and may change! + + License (OLC-3) + ~~~~~~~~~~~~~~~ + + Copyright 2018-2019 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 2018 +*/ + + +#ifndef OLC_PGEX_GFX3D +#define OLC_PGEX_GFX3D + +#include +#include +#include +#undef min +#undef max + +namespace olc +{ + // Container class for Advanced 2D Drawing functions + class GFX3D : public olc::PGEX + { + + public: + + struct vec2d + { + float x = 0; + float y = 0; + float z = 0; + }; + + struct vec3d + { + float x = 0; + float y = 0; + float z = 0; + float w = 1; // Need a 4th term to perform sensible matrix vector multiplication + }; + + struct triangle + { + vec3d p[3]; + vec2d t[3]; + olc::Pixel col; + }; + + struct mat4x4 + { + float m[4][4] = { 0 }; + }; + + struct mesh + { + std::vector tris; + }; + + class Math + { + public: + inline Math(); + public: + inline static vec3d Mat_MultiplyVector(mat4x4 &m, vec3d &i); + inline static mat4x4 Mat_MultiplyMatrix(mat4x4 &m1, mat4x4 &m2); + inline static mat4x4 Mat_MakeIdentity(); + inline static mat4x4 Mat_MakeRotationX(float fAngleRad); + inline static mat4x4 Mat_MakeRotationY(float fAngleRad); + inline static mat4x4 Mat_MakeRotationZ(float fAngleRad); + inline static mat4x4 Mat_MakeScale(float x, float y, float z); + inline static mat4x4 Mat_MakeTranslation(float x, float y, float z); + inline static mat4x4 Mat_MakeProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar); + inline static mat4x4 Mat_PointAt(vec3d &pos, vec3d &target, vec3d &up); + inline static mat4x4 Mat_QuickInverse(mat4x4 &m); // Only for Rotation/Translation Matrices + inline static mat4x4 Mat_Inverse(olc::GFX3D::mat4x4 &m); + + inline static vec3d Vec_Add(vec3d &v1, vec3d &v2); + inline static vec3d Vec_Sub(vec3d &v1, vec3d &v2); + inline static vec3d Vec_Mul(vec3d &v1, float k); + inline static vec3d Vec_Div(vec3d &v1, float k); + inline static float Vec_DotProduct(vec3d &v1, vec3d &v2); + inline static float Vec_Length(vec3d &v); + inline static vec3d Vec_Normalise(vec3d &v); + inline static vec3d Vec_CrossProduct(vec3d &v1, vec3d &v2); + inline static vec3d Vec_IntersectPlane(vec3d &plane_p, vec3d &plane_n, vec3d &lineStart, vec3d &lineEnd, float &t); + + inline static int Triangle_ClipAgainstPlane(vec3d plane_p, vec3d plane_n, triangle &in_tri, triangle &out_tri1, triangle &out_tri2); + }; + + enum RENDERFLAGS + { + RENDER_WIRE = 0x01, + RENDER_FLAT = 0x02, + RENDER_TEXTURED = 0x04, + RENDER_CULL_CW = 0x08, + RENDER_CULL_CCW = 0x10, + RENDER_DEPTH = 0x20, + }; + + + class PipeLine + { + public: + PipeLine(); + + public: + void SetProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar, float fLeft, float fTop, float fWidth, float fHeight); + void SetCamera(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &lookat, olc::GFX3D::vec3d &up); + void SetTransform(olc::GFX3D::mat4x4 &transform); + void SetTexture(olc::Sprite *texture); + void SetLightSource(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &dir, olc::Pixel &col); + uint32_t Render(std::vector &triangles, uint32_t flags = RENDER_CULL_CW | RENDER_TEXTURED | RENDER_DEPTH); + + private: + olc::GFX3D::mat4x4 matProj; + olc::GFX3D::mat4x4 matView; + olc::GFX3D::mat4x4 matWorld; + olc::Sprite *sprTexture; + float fViewX; + float fViewY; + float fViewW; + float fViewH; + }; + + + + public: + //static const int RF_TEXTURE = 0x00000001; + //static const int RF_ = 0x00000002; + + inline static void ConfigureDisplay(); + inline static void ClearDepth(); + inline static void AddTriangleToScene(olc::GFX3D::triangle &tri); + inline static void RenderScene(); + + inline static void DrawTriangleFlat(olc::GFX3D::triangle &tri); + inline static void DrawTriangleWire(olc::GFX3D::triangle &tri, olc::Pixel col = olc::WHITE); + inline static void DrawTriangleTex(olc::GFX3D::triangle &tri, olc::Sprite* spr); + inline static void TexturedTriangle(int x1, int y1, float u1, float v1, float w1, + int x2, int y2, float u2, float v2, float w2, + int x3, int y3, float u3, float v3, float w3, olc::Sprite* spr); + + // Draws a sprite with the transform applied + //inline static void DrawSprite(olc::Sprite *sprite, olc::GFX2D::Transform2D &transform); + + private: + static float* m_DepthBuffer; + }; +} + + + + +namespace olc +{ + olc::GFX3D::Math::Math() + { + + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Mat_MultiplyVector(olc::GFX3D::mat4x4 &m, olc::GFX3D::vec3d &i) + { + vec3d v; + v.x = i.x * m.m[0][0] + i.y * m.m[1][0] + i.z * m.m[2][0] + i.w * m.m[3][0]; + v.y = i.x * m.m[0][1] + i.y * m.m[1][1] + i.z * m.m[2][1] + i.w * m.m[3][1]; + v.z = i.x * m.m[0][2] + i.y * m.m[1][2] + i.z * m.m[2][2] + i.w * m.m[3][2]; + v.w = i.x * m.m[0][3] + i.y * m.m[1][3] + i.z * m.m[2][3] + i.w * m.m[3][3]; + return v; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeIdentity() + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = 1.0f; + matrix.m[1][1] = 1.0f; + matrix.m[2][2] = 1.0f; + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeRotationX(float fAngleRad) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = 1.0f; + matrix.m[1][1] = cosf(fAngleRad); + matrix.m[1][2] = sinf(fAngleRad); + matrix.m[2][1] = -sinf(fAngleRad); + matrix.m[2][2] = cosf(fAngleRad); + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeRotationY(float fAngleRad) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = cosf(fAngleRad); + matrix.m[0][2] = sinf(fAngleRad); + matrix.m[2][0] = -sinf(fAngleRad); + matrix.m[1][1] = 1.0f; + matrix.m[2][2] = cosf(fAngleRad); + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeRotationZ(float fAngleRad) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = cosf(fAngleRad); + matrix.m[0][1] = sinf(fAngleRad); + matrix.m[1][0] = -sinf(fAngleRad); + matrix.m[1][1] = cosf(fAngleRad); + matrix.m[2][2] = 1.0f; + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeScale(float x, float y, float z) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = x; + matrix.m[1][1] = y; + matrix.m[2][2] = z; + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeTranslation(float x, float y, float z) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = 1.0f; + matrix.m[1][1] = 1.0f; + matrix.m[2][2] = 1.0f; + matrix.m[3][3] = 1.0f; + matrix.m[3][0] = x; + matrix.m[3][1] = y; + matrix.m[3][2] = z; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar) + { + float fFovRad = 1.0f / tanf(fFovDegrees * 0.5f / 180.0f * 3.14159f); + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = fAspectRatio * fFovRad; + matrix.m[1][1] = fFovRad; + matrix.m[2][2] = fFar / (fFar - fNear); + matrix.m[3][2] = (-fFar * fNear) / (fFar - fNear); + matrix.m[2][3] = 1.0f; + matrix.m[3][3] = 0.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MultiplyMatrix(olc::GFX3D::mat4x4 &m1, olc::GFX3D::mat4x4 &m2) + { + olc::GFX3D::mat4x4 matrix; + for (int c = 0; c < 4; c++) + for (int r = 0; r < 4; r++) + matrix.m[r][c] = m1.m[r][0] * m2.m[0][c] + m1.m[r][1] * m2.m[1][c] + m1.m[r][2] * m2.m[2][c] + m1.m[r][3] * m2.m[3][c]; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_PointAt(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &target, olc::GFX3D::vec3d &up) + { + // Calculate new forward direction + olc::GFX3D::vec3d newForward = Vec_Sub(target, pos); + newForward = Vec_Normalise(newForward); + + // Calculate new Up direction + olc::GFX3D::vec3d a = Vec_Mul(newForward, Vec_DotProduct(up, newForward)); + olc::GFX3D::vec3d newUp = Vec_Sub(up, a); + newUp = Vec_Normalise(newUp); + + // New Right direction is easy, its just cross product + olc::GFX3D::vec3d newRight = Vec_CrossProduct(newUp, newForward); + + // Construct Dimensioning and Translation Matrix + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = newRight.x; matrix.m[0][1] = newRight.y; matrix.m[0][2] = newRight.z; matrix.m[0][3] = 0.0f; + matrix.m[1][0] = newUp.x; matrix.m[1][1] = newUp.y; matrix.m[1][2] = newUp.z; matrix.m[1][3] = 0.0f; + matrix.m[2][0] = newForward.x; matrix.m[2][1] = newForward.y; matrix.m[2][2] = newForward.z; matrix.m[2][3] = 0.0f; + matrix.m[3][0] = pos.x; matrix.m[3][1] = pos.y; matrix.m[3][2] = pos.z; matrix.m[3][3] = 1.0f; + return matrix; + + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_QuickInverse(olc::GFX3D::mat4x4 &m) // Only for Rotation/Translation Matrices + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = m.m[0][0]; matrix.m[0][1] = m.m[1][0]; matrix.m[0][2] = m.m[2][0]; matrix.m[0][3] = 0.0f; + matrix.m[1][0] = m.m[0][1]; matrix.m[1][1] = m.m[1][1]; matrix.m[1][2] = m.m[2][1]; matrix.m[1][3] = 0.0f; + matrix.m[2][0] = m.m[0][2]; matrix.m[2][1] = m.m[1][2]; matrix.m[2][2] = m.m[2][2]; matrix.m[2][3] = 0.0f; + matrix.m[3][0] = -(m.m[3][0] * matrix.m[0][0] + m.m[3][1] * matrix.m[1][0] + m.m[3][2] * matrix.m[2][0]); + matrix.m[3][1] = -(m.m[3][0] * matrix.m[0][1] + m.m[3][1] * matrix.m[1][1] + m.m[3][2] * matrix.m[2][1]); + matrix.m[3][2] = -(m.m[3][0] * matrix.m[0][2] + m.m[3][1] * matrix.m[1][2] + m.m[3][2] * matrix.m[2][2]); + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_Inverse(olc::GFX3D::mat4x4 &m) + { + double det; + + + mat4x4 matInv; + + matInv.m[0][0] = m.m[1][1] * m.m[2][2] * m.m[3][3] - m.m[1][1] * m.m[2][3] * m.m[3][2] - m.m[2][1] * m.m[1][2] * m.m[3][3] + m.m[2][1] * m.m[1][3] * m.m[3][2] + m.m[3][1] * m.m[1][2] * m.m[2][3] - m.m[3][1] * m.m[1][3] * m.m[2][2]; + matInv.m[1][0] = -m.m[1][0] * m.m[2][2] * m.m[3][3] + m.m[1][0] * m.m[2][3] * m.m[3][2] + m.m[2][0] * m.m[1][2] * m.m[3][3] - m.m[2][0] * m.m[1][3] * m.m[3][2] - m.m[3][0] * m.m[1][2] * m.m[2][3] + m.m[3][0] * m.m[1][3] * m.m[2][2]; + matInv.m[2][0] = m.m[1][0] * m.m[2][1] * m.m[3][3] - m.m[1][0] * m.m[2][3] * m.m[3][1] - m.m[2][0] * m.m[1][1] * m.m[3][3] + m.m[2][0] * m.m[1][3] * m.m[3][1] + m.m[3][0] * m.m[1][1] * m.m[2][3] - m.m[3][0] * m.m[1][3] * m.m[2][1]; + matInv.m[3][0] = -m.m[1][0] * m.m[2][1] * m.m[3][2] + m.m[1][0] * m.m[2][2] * m.m[3][1] + m.m[2][0] * m.m[1][1] * m.m[3][2] - m.m[2][0] * m.m[1][2] * m.m[3][1] - m.m[3][0] * m.m[1][1] * m.m[2][2] + m.m[3][0] * m.m[1][2] * m.m[2][1]; + matInv.m[0][1] = -m.m[0][1] * m.m[2][2] * m.m[3][3] + m.m[0][1] * m.m[2][3] * m.m[3][2] + m.m[2][1] * m.m[0][2] * m.m[3][3] - m.m[2][1] * m.m[0][3] * m.m[3][2] - m.m[3][1] * m.m[0][2] * m.m[2][3] + m.m[3][1] * m.m[0][3] * m.m[2][2]; + matInv.m[1][1] = m.m[0][0] * m.m[2][2] * m.m[3][3] - m.m[0][0] * m.m[2][3] * m.m[3][2] - m.m[2][0] * m.m[0][2] * m.m[3][3] + m.m[2][0] * m.m[0][3] * m.m[3][2] + m.m[3][0] * m.m[0][2] * m.m[2][3] - m.m[3][0] * m.m[0][3] * m.m[2][2]; + matInv.m[2][1] = -m.m[0][0] * m.m[2][1] * m.m[3][3] + m.m[0][0] * m.m[2][3] * m.m[3][1] + m.m[2][0] * m.m[0][1] * m.m[3][3] - m.m[2][0] * m.m[0][3] * m.m[3][1] - m.m[3][0] * m.m[0][1] * m.m[2][3] + m.m[3][0] * m.m[0][3] * m.m[2][1]; + matInv.m[3][1] = m.m[0][0] * m.m[2][1] * m.m[3][2] - m.m[0][0] * m.m[2][2] * m.m[3][1] - m.m[2][0] * m.m[0][1] * m.m[3][2] + m.m[2][0] * m.m[0][2] * m.m[3][1] + m.m[3][0] * m.m[0][1] * m.m[2][2] - m.m[3][0] * m.m[0][2] * m.m[2][1]; + matInv.m[0][2] = m.m[0][1] * m.m[1][2] * m.m[3][3] - m.m[0][1] * m.m[1][3] * m.m[3][2] - m.m[1][1] * m.m[0][2] * m.m[3][3] + m.m[1][1] * m.m[0][3] * m.m[3][2] + m.m[3][1] * m.m[0][2] * m.m[1][3] - m.m[3][1] * m.m[0][3] * m.m[1][2]; + matInv.m[1][2] = -m.m[0][0] * m.m[1][2] * m.m[3][3] + m.m[0][0] * m.m[1][3] * m.m[3][2] + m.m[1][0] * m.m[0][2] * m.m[3][3] - m.m[1][0] * m.m[0][3] * m.m[3][2] - m.m[3][0] * m.m[0][2] * m.m[1][3] + m.m[3][0] * m.m[0][3] * m.m[1][2]; + matInv.m[2][2] = m.m[0][0] * m.m[1][1] * m.m[3][3] - m.m[0][0] * m.m[1][3] * m.m[3][1] - m.m[1][0] * m.m[0][1] * m.m[3][3] + m.m[1][0] * m.m[0][3] * m.m[3][1] + m.m[3][0] * m.m[0][1] * m.m[1][3] - m.m[3][0] * m.m[0][3] * m.m[1][1]; + matInv.m[3][2] = -m.m[0][0] * m.m[1][1] * m.m[3][2] + m.m[0][0] * m.m[1][2] * m.m[3][1] + m.m[1][0] * m.m[0][1] * m.m[3][2] - m.m[1][0] * m.m[0][2] * m.m[3][1] - m.m[3][0] * m.m[0][1] * m.m[1][2] + m.m[3][0] * m.m[0][2] * m.m[1][1]; + matInv.m[0][3] = -m.m[0][1] * m.m[1][2] * m.m[2][3] + m.m[0][1] * m.m[1][3] * m.m[2][2] + m.m[1][1] * m.m[0][2] * m.m[2][3] - m.m[1][1] * m.m[0][3] * m.m[2][2] - m.m[2][1] * m.m[0][2] * m.m[1][3] + m.m[2][1] * m.m[0][3] * m.m[1][2]; + matInv.m[1][3] = m.m[0][0] * m.m[1][2] * m.m[2][3] - m.m[0][0] * m.m[1][3] * m.m[2][2] - m.m[1][0] * m.m[0][2] * m.m[2][3] + m.m[1][0] * m.m[0][3] * m.m[2][2] + m.m[2][0] * m.m[0][2] * m.m[1][3] - m.m[2][0] * m.m[0][3] * m.m[1][2]; + matInv.m[2][3] = -m.m[0][0] * m.m[1][1] * m.m[2][3] + m.m[0][0] * m.m[1][3] * m.m[2][1] + m.m[1][0] * m.m[0][1] * m.m[2][3] - m.m[1][0] * m.m[0][3] * m.m[2][1] - m.m[2][0] * m.m[0][1] * m.m[1][3] + m.m[2][0] * m.m[0][3] * m.m[1][1]; + matInv.m[3][3] = m.m[0][0] * m.m[1][1] * m.m[2][2] - m.m[0][0] * m.m[1][2] * m.m[2][1] - m.m[1][0] * m.m[0][1] * m.m[2][2] + m.m[1][0] * m.m[0][2] * m.m[2][1] + m.m[2][0] * m.m[0][1] * m.m[1][2] - m.m[2][0] * m.m[0][2] * m.m[1][1]; + + det = m.m[0][0] * matInv.m[0][0] + m.m[0][1] * matInv.m[1][0] + m.m[0][2] * matInv.m[2][0] + m.m[0][3] * matInv.m[3][0]; + // if (det == 0) return false; + + det = 1.0 / det; + + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + matInv.m[i][j] *= (float)det; + + return matInv; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Add(olc::GFX3D::vec3d &v1, olc::GFX3D::vec3d &v2) + { + return { v1.x + v2.x, v1.y + v2.y, v1.z + v2.z }; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Sub(olc::GFX3D::vec3d &v1, olc::GFX3D::vec3d &v2) + { + return { v1.x - v2.x, v1.y - v2.y, v1.z - v2.z }; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Mul(olc::GFX3D::vec3d &v1, float k) + { + return { v1.x * k, v1.y * k, v1.z * k }; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Div(olc::GFX3D::vec3d &v1, float k) + { + return { v1.x / k, v1.y / k, v1.z / k }; + } + + float olc::GFX3D::Math::Vec_DotProduct(olc::GFX3D::vec3d &v1, olc::GFX3D::vec3d &v2) + { + return v1.x*v2.x + v1.y*v2.y + v1.z * v2.z; + } + + float olc::GFX3D::Math::Vec_Length(olc::GFX3D::vec3d &v) + { + return sqrtf(Vec_DotProduct(v, v)); + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Normalise(olc::GFX3D::vec3d &v) + { + float l = Vec_Length(v); + return { v.x / l, v.y / l, v.z / l }; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_CrossProduct(olc::GFX3D::vec3d &v1, olc::GFX3D::vec3d &v2) + { + vec3d v; + v.x = v1.y * v2.z - v1.z * v2.y; + v.y = v1.z * v2.x - v1.x * v2.z; + v.z = v1.x * v2.y - v1.y * v2.x; + return v; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_IntersectPlane(olc::GFX3D::vec3d &plane_p, olc::GFX3D::vec3d &plane_n, olc::GFX3D::vec3d &lineStart, olc::GFX3D::vec3d &lineEnd, float &t) + { + plane_n = Vec_Normalise(plane_n); + float plane_d = -Vec_DotProduct(plane_n, plane_p); + float ad = Vec_DotProduct(lineStart, plane_n); + float bd = Vec_DotProduct(lineEnd, plane_n); + t = (-plane_d - ad) / (bd - ad); + olc::GFX3D::vec3d lineStartToEnd = Vec_Sub(lineEnd, lineStart); + olc::GFX3D::vec3d lineToIntersect = Vec_Mul(lineStartToEnd, t); + return Vec_Add(lineStart, lineToIntersect); + } + + + int olc::GFX3D::Math::Triangle_ClipAgainstPlane(vec3d plane_p, vec3d plane_n, triangle &in_tri, triangle &out_tri1, triangle &out_tri2) + { + // Make sure plane normal is indeed normal + plane_n = Math::Vec_Normalise(plane_n); + + out_tri1.t[0] = in_tri.t[0]; + out_tri2.t[0] = in_tri.t[0]; + out_tri1.t[1] = in_tri.t[1]; + out_tri2.t[1] = in_tri.t[1]; + out_tri1.t[2] = in_tri.t[2]; + out_tri2.t[2] = in_tri.t[2]; + + // Return signed shortest distance from point to plane, plane normal must be normalised + auto dist = [&](vec3d &p) + { + vec3d n = Math::Vec_Normalise(p); + return (plane_n.x * p.x + plane_n.y * p.y + plane_n.z * p.z - Math::Vec_DotProduct(plane_n, plane_p)); + }; + + // Create two temporary storage arrays to classify points either side of plane + // If distance sign is positive, point lies on "inside" of plane + vec3d* inside_points[3]; int nInsidePointCount = 0; + vec3d* outside_points[3]; int nOutsidePointCount = 0; + vec2d* inside_tex[3]; int nInsideTexCount = 0; + vec2d* outside_tex[3]; int nOutsideTexCount = 0; + + + // Get signed distance of each point in triangle to plane + float d0 = dist(in_tri.p[0]); + float d1 = dist(in_tri.p[1]); + float d2 = dist(in_tri.p[2]); + + if (d0 >= 0) { inside_points[nInsidePointCount++] = &in_tri.p[0]; inside_tex[nInsideTexCount++] = &in_tri.t[0]; } + else { + outside_points[nOutsidePointCount++] = &in_tri.p[0]; outside_tex[nOutsideTexCount++] = &in_tri.t[0]; + } + if (d1 >= 0) { + inside_points[nInsidePointCount++] = &in_tri.p[1]; inside_tex[nInsideTexCount++] = &in_tri.t[1]; + } + else { + outside_points[nOutsidePointCount++] = &in_tri.p[1]; outside_tex[nOutsideTexCount++] = &in_tri.t[1]; + } + if (d2 >= 0) { + inside_points[nInsidePointCount++] = &in_tri.p[2]; inside_tex[nInsideTexCount++] = &in_tri.t[2]; + } + else { + outside_points[nOutsidePointCount++] = &in_tri.p[2]; outside_tex[nOutsideTexCount++] = &in_tri.t[2]; + } + + // Now classify triangle points, and break the input triangle into + // smaller output triangles if required. There are four possible + // outcomes... + + if (nInsidePointCount == 0) + { + // All points lie on the outside of plane, so clip whole triangle + // It ceases to exist + + return 0; // No returned triangles are valid + } + + if (nInsidePointCount == 3) + { + // All points lie on the inside of plane, so do nothing + // and allow the triangle to simply pass through + out_tri1 = in_tri; + + return 1; // Just the one returned original triangle is valid + } + + if (nInsidePointCount == 1 && nOutsidePointCount == 2) + { + // Triangle should be clipped. As two points lie outside + // the plane, the triangle simply becomes a smaller triangle + + // Copy appearance info to new triangle + out_tri1.col = olc::MAGENTA;// in_tri.col; + + // The inside point is valid, so keep that... + out_tri1.p[0] = *inside_points[0]; + out_tri1.t[0] = *inside_tex[0]; + + // but the two new points are at the locations where the + // original sides of the triangle (lines) intersect with the plane + float t; + out_tri1.p[1] = Math::Vec_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[0], t); + out_tri1.t[1].x = t * (outside_tex[0]->x - inside_tex[0]->x) + inside_tex[0]->x; + out_tri1.t[1].y = t * (outside_tex[0]->y - inside_tex[0]->y) + inside_tex[0]->y; + out_tri1.t[1].z = t * (outside_tex[0]->z - inside_tex[0]->z) + inside_tex[0]->z; + + out_tri1.p[2] = Math::Vec_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[1], t); + out_tri1.t[2].x = t * (outside_tex[1]->x - inside_tex[0]->x) + inside_tex[0]->x; + out_tri1.t[2].y = t * (outside_tex[1]->y - inside_tex[0]->y) + inside_tex[0]->y; + out_tri1.t[2].z = t * (outside_tex[1]->z - inside_tex[0]->z) + inside_tex[0]->z; + + return 1; // Return the newly formed single triangle + } + + if (nInsidePointCount == 2 && nOutsidePointCount == 1) + { + // Triangle should be clipped. As two points lie inside the plane, + // the clipped triangle becomes a "quad". Fortunately, we can + // represent a quad with two new triangles + + // Copy appearance info to new triangles + out_tri1.col = olc::GREEN;// in_tri.col; + out_tri2.col = olc::RED;// in_tri.col; + + // The first triangle consists of the two inside points and a new + // point determined by the location where one side of the triangle + // intersects with the plane + out_tri1.p[0] = *inside_points[0]; + out_tri1.t[0] = *inside_tex[0]; + + out_tri1.p[1] = *inside_points[1]; + out_tri1.t[1] = *inside_tex[1]; + + float t; + out_tri1.p[2] = Math::Vec_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[0], t); + out_tri1.t[2].x = t * (outside_tex[0]->x - inside_tex[0]->x) + inside_tex[0]->x; + out_tri1.t[2].y = t * (outside_tex[0]->y - inside_tex[0]->y) + inside_tex[0]->y; + out_tri1.t[2].z = t * (outside_tex[0]->z - inside_tex[0]->z) + inside_tex[0]->z; + + // The second triangle is composed of one of he inside points, a + // new point determined by the intersection of the other side of the + // triangle and the plane, and the newly created point above + out_tri2.p[1] = *inside_points[1]; + out_tri2.t[1] = *inside_tex[1]; + out_tri2.p[0] = out_tri1.p[2]; + out_tri2.t[0] = out_tri1.t[2]; + out_tri2.p[2] = Math::Vec_IntersectPlane(plane_p, plane_n, *inside_points[1], *outside_points[0], t); + out_tri2.t[2].x = t * (outside_tex[0]->x - inside_tex[1]->x) + inside_tex[1]->x; + out_tri2.t[2].y = t * (outside_tex[0]->y - inside_tex[1]->y) + inside_tex[1]->y; + out_tri2.t[2].z = t * (outside_tex[0]->z - inside_tex[1]->z) + inside_tex[1]->z; + return 2; // Return two newly formed triangles which form a quad + } + + return 0; + } + + void GFX3D::DrawTriangleFlat(olc::GFX3D::triangle &tri) + { + pge->FillTriangle(tri.p[0].x, tri.p[0].y, tri.p[1].x, tri.p[1].y, tri.p[2].x, tri.p[2].y, tri.col); + } + + void GFX3D::DrawTriangleWire(olc::GFX3D::triangle &tri, olc::Pixel col) + { + pge->DrawTriangle(tri.p[0].x, tri.p[0].y, tri.p[1].x, tri.p[1].y, tri.p[2].x, tri.p[2].y, col); + } + + void GFX3D::TexturedTriangle(int x1, int y1, float u1, float v1, float w1, + int x2, int y2, float u2, float v2, float w2, + int x3, int y3, float u3, float v3, float w3, olc::Sprite* spr) + + { + if (y2 < y1) + { + std::swap(y1, y2); + std::swap(x1, x2); + std::swap(u1, u2); + std::swap(v1, v2); + std::swap(w1, w2); + } + + if (y3 < y1) + { + std::swap(y1, y3); + std::swap(x1, x3); + std::swap(u1, u3); + std::swap(v1, v3); + std::swap(w1, w3); + } + + if (y3 < y2) + { + std::swap(y2, y3); + std::swap(x2, x3); + std::swap(u2, u3); + std::swap(v2, v3); + std::swap(w2, w3); + } + + int dy1 = y2 - y1; + int dx1 = x2 - x1; + float dv1 = v2 - v1; + float du1 = u2 - u1; + float dw1 = w2 - w1; + + int dy2 = y3 - y1; + int dx2 = x3 - x1; + float dv2 = v3 - v1; + float du2 = u3 - u1; + float dw2 = w3 - w1; + + float tex_u, tex_v, tex_w; + + float dax_step = 0, dbx_step = 0, + du1_step = 0, dv1_step = 0, + du2_step = 0, dv2_step = 0, + dw1_step = 0, dw2_step = 0; + + if (dy1) dax_step = dx1 / (float)abs(dy1); + if (dy2) dbx_step = dx2 / (float)abs(dy2); + + if (dy1) du1_step = du1 / (float)abs(dy1); + if (dy1) dv1_step = dv1 / (float)abs(dy1); + if (dy1) dw1_step = dw1 / (float)abs(dy1); + + if (dy2) du2_step = du2 / (float)abs(dy2); + if (dy2) dv2_step = dv2 / (float)abs(dy2); + if (dy2) dw2_step = dw2 / (float)abs(dy2); + + if (dy1) + { + for (int i = y1; i <= y2; i++) + { + int ax = x1 + (float)(i - y1) * dax_step; + int bx = x1 + (float)(i - y1) * dbx_step; + + float tex_su = u1 + (float)(i - y1) * du1_step; + float tex_sv = v1 + (float)(i - y1) * dv1_step; + float tex_sw = w1 + (float)(i - y1) * dw1_step; + + float tex_eu = u1 + (float)(i - y1) * du2_step; + float tex_ev = v1 + (float)(i - y1) * dv2_step; + float tex_ew = w1 + (float)(i - y1) * dw2_step; + + if (ax > bx) + { + std::swap(ax, bx); + std::swap(tex_su, tex_eu); + std::swap(tex_sv, tex_ev); + std::swap(tex_sw, tex_ew); + } + + tex_u = tex_su; + tex_v = tex_sv; + tex_w = tex_sw; + + float tstep = 1.0f / ((float)(bx - ax)); + float t = 0.0f; + + for (int j = ax; j < bx; j++) + { + tex_u = (1.0f - t) * tex_su + t * tex_eu; + tex_v = (1.0f - t) * tex_sv + t * tex_ev; + tex_w = (1.0f - t) * tex_sw + t * tex_ew; + if (tex_w > m_DepthBuffer[i*pge->ScreenWidth() + j]) + { + pge->Draw(j, i, spr->Sample(tex_u / tex_w, tex_v / tex_w)); + m_DepthBuffer[i*pge->ScreenWidth() + j] = tex_w; + } + t += tstep; + } + + } + } + + dy1 = y3 - y2; + dx1 = x3 - x2; + dv1 = v3 - v2; + du1 = u3 - u2; + dw1 = w3 - w2; + + if (dy1) dax_step = dx1 / (float)abs(dy1); + if (dy2) dbx_step = dx2 / (float)abs(dy2); + + du1_step = 0, dv1_step = 0; + if (dy1) du1_step = du1 / (float)abs(dy1); + if (dy1) dv1_step = dv1 / (float)abs(dy1); + if (dy1) dw1_step = dw1 / (float)abs(dy1); + + if (dy1) + { + for (int i = y2; i <= y3; i++) + { + int ax = x2 + (float)(i - y2) * dax_step; + int bx = x1 + (float)(i - y1) * dbx_step; + + float tex_su = u2 + (float)(i - y2) * du1_step; + float tex_sv = v2 + (float)(i - y2) * dv1_step; + float tex_sw = w2 + (float)(i - y2) * dw1_step; + + float tex_eu = u1 + (float)(i - y1) * du2_step; + float tex_ev = v1 + (float)(i - y1) * dv2_step; + float tex_ew = w1 + (float)(i - y1) * dw2_step; + + if (ax > bx) + { + std::swap(ax, bx); + std::swap(tex_su, tex_eu); + std::swap(tex_sv, tex_ev); + std::swap(tex_sw, tex_ew); + } + + tex_u = tex_su; + tex_v = tex_sv; + tex_w = tex_sw; + + float tstep = 1.0f / ((float)(bx - ax)); + float t = 0.0f; + + for (int j = ax; j < bx; j++) + { + tex_u = (1.0f - t) * tex_su + t * tex_eu; + tex_v = (1.0f - t) * tex_sv + t * tex_ev; + tex_w = (1.0f - t) * tex_sw + t * tex_ew; + + if (tex_w > m_DepthBuffer[i*pge->ScreenWidth() + j]) + { + pge->Draw(j, i, spr->Sample(tex_u / tex_w, tex_v / tex_w)); + m_DepthBuffer[i*pge->ScreenWidth() + j] = tex_w; + } + t += tstep; + } + } + } + } + + + void GFX3D::DrawTriangleTex(olc::GFX3D::triangle &tri, olc::Sprite* spr) + { + if (tri.p[1].y < tri.p[0].y) + { + std::swap(tri.p[0].y, tri.p[1].y); + std::swap(tri.p[0].x, tri.p[1].x); + std::swap(tri.t[0].x, tri.t[1].x); + std::swap(tri.t[0].y, tri.t[1].y); + std::swap(tri.t[0].z, tri.t[1].z); + } + + if (tri.p[2].y < tri.p[0].y) + { + std::swap(tri.p[0].y, tri.p[2].y); + std::swap(tri.p[0].x, tri.p[2].x); + std::swap(tri.t[0].x, tri.t[2].x); + std::swap(tri.t[0].y, tri.t[2].y); + std::swap(tri.t[0].z, tri.t[2].z); + } + + if (tri.p[2].y < tri.p[1].y) + { + std::swap(tri.p[1].y, tri.p[2].y); + std::swap(tri.p[1].x, tri.p[2].x); + std::swap(tri.t[1].x, tri.t[2].x); + std::swap(tri.t[1].y, tri.t[2].y); + std::swap(tri.t[1].z, tri.t[2].z); + } + + int dy1 = tri.p[1].y - tri.p[0].y; + int dx1 = tri.p[1].x - tri.p[0].x; + float dv1 = tri.t[1].y - tri.t[0].y; + float du1 = tri.t[1].x - tri.t[0].x; + float dz1 = tri.t[1].z - tri.t[0].z; + + int dy2 = tri.p[2].y - tri.p[0].y; + int dx2 = tri.p[2].x - tri.p[0].x; + float dv2 = tri.t[2].y - tri.t[0].y; + float du2 = tri.t[2].x - tri.t[0].x; + float dz2 = tri.t[2].z - tri.t[0].z; + + float tex_x, tex_y, tex_z; + + float du1_step = 0, dv1_step = 0, du2_step = 0, dv2_step = 0, dz1_step = 0, dz2_step = 0; + float dax_step = 0, dbx_step = 0; + + if (dy1) dax_step = dx1 / (float)abs(dy1); + if (dy2) dbx_step = dx2 / (float)abs(dy2); + + if (dy1) du1_step = du1 / (float)abs(dy1); + if (dy1) dv1_step = dv1 / (float)abs(dy1); + if (dy1) dz1_step = dz1 / (float)abs(dy1); + + if (dy2) du2_step = du2 / (float)abs(dy2); + if (dy2) dv2_step = dv2 / (float)abs(dy2); + if (dy2) dz2_step = dz2 / (float)abs(dy2); + + + + if (dy1) + { + for (int i = tri.p[0].y; i <= tri.p[1].y; i++) + { + int ax = tri.p[0].x + (i - tri.p[0].y) * dax_step; + int bx = tri.p[0].x + (i - tri.p[0].y) * dbx_step; + + // Start and end points in texture space + float tex_su = tri.t[0].x + (float)(i - tri.p[0].y) * du1_step; + float tex_sv = tri.t[0].y + (float)(i - tri.p[0].y) * dv1_step; + float tex_sz = tri.t[0].z + (float)(i - tri.p[0].y) * dz1_step; + + float tex_eu = tri.t[0].x + (float)(i - tri.p[0].y) * du2_step; + float tex_ev = tri.t[0].y + (float)(i - tri.p[0].y) * dv2_step; + float tex_ez = tri.t[0].z + (float)(i - tri.p[0].y) * dz2_step; + + if (ax > bx) + { + std::swap(ax, bx); + std::swap(tex_su, tex_eu); + std::swap(tex_sv, tex_ev); + std::swap(tex_sz, tex_ez); + } + + tex_x = tex_su; + tex_y = tex_sv; + tex_z = tex_sz; + + + float tstep = 1.0f / ((float)(bx - ax)); + float t = 0; + + for (int j = ax; j < bx; j++) + { + tex_x = (1.0f - t) * tex_su + t * tex_eu; + tex_y = (1.0f - t) * tex_sv + t * tex_ev; + tex_z = (1.0f - t) * tex_sz + t * tex_ez; + + if (tex_z > m_DepthBuffer[i*pge->ScreenWidth() + j]) + { + pge->Draw(j, i, spr->Sample(tex_x / tex_z, tex_y / tex_z)); + m_DepthBuffer[i*pge->ScreenWidth() + j] = tex_z; + } + t += tstep; + } + + + } + } + + dy1 = tri.p[2].y - tri.p[1].y; + dx1 = tri.p[2].x - tri.p[1].x; + dv1 = tri.t[2].y - tri.t[1].y; + du1 = tri.t[2].x - tri.t[1].x; + dz1 = tri.t[2].z - tri.t[1].z; + + if (dy1) dax_step = dx1 / (float)abs(dy1); + if (dy2) dbx_step = dx2 / (float)abs(dy2); + + + du1_step = 0, dv1_step = 0;// , dz1_step = 0;// , du2_step = 0, dv2_step = 0; + if (dy1) du1_step = du1 / (float)abs(dy1); + if (dy1) dv1_step = dv1 / (float)abs(dy1); + if (dy1) dz1_step = dz1 / (float)abs(dy1); + + if (dy1) + { + for (int i = tri.p[1].y; i <= tri.p[2].y; i++) + { + int ax = tri.p[1].x + (i - tri.p[1].y) * dax_step; + int bx = tri.p[0].x + (i - tri.p[0].y) * dbx_step; + + // Start and end points in texture space + float tex_su = tri.t[1].x + (float)(i - tri.p[1].y) * du1_step; + float tex_sv = tri.t[1].y + (float)(i - tri.p[1].y) * dv1_step; + float tex_sz = tri.t[1].z + (float)(i - tri.p[1].y) * dz1_step; + + float tex_eu = tri.t[0].x + (float)(i - tri.p[0].y) * du2_step; + float tex_ev = tri.t[0].y + (float)(i - tri.p[0].y) * dv2_step; + float tex_ez = tri.t[0].z + (float)(i - tri.p[0].y) * dz2_step; + + if (ax > bx) + { + std::swap(ax, bx); + std::swap(tex_su, tex_eu); + std::swap(tex_sv, tex_ev); + std::swap(tex_sz, tex_ez); + } + + tex_x = tex_su; + tex_y = tex_sv; + tex_z = tex_sz; + + + float tstep = 1.0f / ((float)(bx - ax)); + float t = 0; + + for (int j = ax; j < bx; j++) + { + tex_x = (1.0f - t) * tex_su + t * tex_eu; + tex_y = (1.0f - t) * tex_sv + t * tex_ev; + tex_z = (1.0f - t) * tex_sz + t * tex_ez; + + if (tex_z > m_DepthBuffer[i*pge->ScreenWidth() + j]) + { + pge->Draw(j, i, spr->Sample(tex_x / tex_z, tex_y / tex_z)); + m_DepthBuffer[i*pge->ScreenWidth() + j] = tex_z; + } + + t += tstep; + } + } + } + + } + + float* GFX3D::m_DepthBuffer = nullptr; + + void GFX3D::ConfigureDisplay() + { + m_DepthBuffer = new float[pge->ScreenWidth() * pge->ScreenHeight()]{ 0 }; + } + + + void GFX3D::ClearDepth() + { + memset(m_DepthBuffer, 0, pge->ScreenWidth() * pge->ScreenHeight() * sizeof(float)); + } + + + + + GFX3D::PipeLine::PipeLine() + { + + } + + void GFX3D::PipeLine::SetProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar, float fLeft, float fTop, float fWidth, float fHeight) + { + matProj = GFX3D::Math::Mat_MakeProjection(fFovDegrees, fAspectRatio, fNear, fFar); + fViewX = fLeft; + fViewY = fTop; + fViewW = fWidth; + fViewH = fHeight; + } + + void GFX3D::PipeLine::SetCamera(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &lookat, olc::GFX3D::vec3d &up) + { + matView = GFX3D::Math::Mat_PointAt(pos, lookat, up); + matView = GFX3D::Math::Mat_QuickInverse(matView); + } + + void GFX3D::PipeLine::SetTransform(olc::GFX3D::mat4x4 &transform) + { + matWorld = transform; + } + + void GFX3D::PipeLine::SetTexture(olc::Sprite *texture) + { + sprTexture = texture; + } + + void GFX3D::PipeLine::SetLightSource(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &dir, olc::Pixel &col) + { + + } + + uint32_t GFX3D::PipeLine::Render(std::vector &triangles, uint32_t flags) + { + // Calculate Transformation Matrix + mat4x4 matWorldView = Math::Mat_MultiplyMatrix(matWorld, matView); + //matWorldViewProj = Math::Mat_MultiplyMatrix(matWorldView, matProj); + + // Store triangles for rastering later + std::vector vecTrianglesToRaster; + + int nTriangleDrawnCount = 0; + + // Process Triangles + for (auto &tri : triangles) + { + GFX3D::triangle triTransformed; + + // Just copy through texture coordinates + triTransformed.t[0] = { tri.t[0].x, tri.t[0].y, tri.t[0].z }; + triTransformed.t[1] = { tri.t[1].x, tri.t[1].y, tri.t[1].z }; + triTransformed.t[2] = { tri.t[2].x, tri.t[2].y, tri.t[2].z }; // Think! + + // Transform Triangle from object into projected space + triTransformed.p[0] = GFX3D::Math::Mat_MultiplyVector(matWorldView, tri.p[0]); + triTransformed.p[1] = GFX3D::Math::Mat_MultiplyVector(matWorldView, tri.p[1]); + triTransformed.p[2] = GFX3D::Math::Mat_MultiplyVector(matWorldView, tri.p[2]); + + // Calculate Triangle Normal in WorldView Space + GFX3D::vec3d normal, line1, line2; + line1 = GFX3D::Math::Vec_Sub(triTransformed.p[1], triTransformed.p[0]); + line2 = GFX3D::Math::Vec_Sub(triTransformed.p[2], triTransformed.p[0]); + normal = GFX3D::Math::Vec_CrossProduct(line1, line2); + normal = GFX3D::Math::Vec_Normalise(normal); + + // Cull triangles that face away from viewer + if (flags & RENDER_CULL_CW && GFX3D::Math::Vec_DotProduct(normal, triTransformed.p[0]) > 0.0f) continue; + if (flags & RENDER_CULL_CCW && GFX3D::Math::Vec_DotProduct(normal, triTransformed.p[0]) < 0.0f) continue; + + // If Lighting, calculate shading + triTransformed.col = olc::WHITE; + + // Clip triangle against near plane + int nClippedTriangles = 0; + triangle clipped[2]; + nClippedTriangles = GFX3D::Math::Triangle_ClipAgainstPlane({ 0.0f, 0.0f, 0.1f }, { 0.0f, 0.0f, 1.0f }, triTransformed, clipped[0], clipped[1]); + + // This may yield two new triangles + for (int n = 0; n < nClippedTriangles; n++) + { + triangle triProjected = clipped[n]; + + // Project new triangle + triProjected.p[0] = GFX3D::Math::Mat_MultiplyVector(matProj, clipped[n].p[0]); + triProjected.p[1] = GFX3D::Math::Mat_MultiplyVector(matProj, clipped[n].p[1]); + triProjected.p[2] = GFX3D::Math::Mat_MultiplyVector(matProj, clipped[n].p[2]); + + // Apply Projection to Verts + triProjected.p[0].x = triProjected.p[0].x / triProjected.p[0].w; + triProjected.p[1].x = triProjected.p[1].x / triProjected.p[1].w; + triProjected.p[2].x = triProjected.p[2].x / triProjected.p[2].w; + + triProjected.p[0].y = triProjected.p[0].y / triProjected.p[0].w; + triProjected.p[1].y = triProjected.p[1].y / triProjected.p[1].w; + triProjected.p[2].y = triProjected.p[2].y / triProjected.p[2].w; + + triProjected.p[0].z = triProjected.p[0].z / triProjected.p[0].w; + triProjected.p[1].z = triProjected.p[1].z / triProjected.p[1].w; + triProjected.p[2].z = triProjected.p[2].z / triProjected.p[2].w; + + // Apply Projection to Tex coords + triProjected.t[0].x = triProjected.t[0].x / triProjected.p[0].w; + triProjected.t[1].x = triProjected.t[1].x / triProjected.p[1].w; + triProjected.t[2].x = triProjected.t[2].x / triProjected.p[2].w; + + triProjected.t[0].y = triProjected.t[0].y / triProjected.p[0].w; + triProjected.t[1].y = triProjected.t[1].y / triProjected.p[1].w; + triProjected.t[2].y = triProjected.t[2].y / triProjected.p[2].w; + + triProjected.t[0].z = 1.0f / triProjected.p[0].w; + triProjected.t[1].z = 1.0f / triProjected.p[1].w; + triProjected.t[2].z = 1.0f / triProjected.p[2].w; + + // Clip against viewport in screen space + // Clip triangles against all four screen edges, this could yield + // a bunch of triangles, so create a queue that we traverse to + // ensure we only test new triangles generated against planes + triangle sclipped[2]; + std::list listTriangles; + + + // Add initial triangle + listTriangles.push_back(triProjected); + int nNewTriangles = 1; + + for (int p = 0; p < 4; p++) + { + int nTrisToAdd = 0; + while (nNewTriangles > 0) + { + // Take triangle from front of queue + triangle test = listTriangles.front(); + listTriangles.pop_front(); + nNewTriangles--; + + // Clip it against a plane. We only need to test each + // subsequent plane, against subsequent new triangles + // as all triangles after a plane clip are guaranteed + // to lie on the inside of the plane. I like how this + // comment is almost completely and utterly justified + switch (p) + { + case 0: nTrisToAdd = GFX3D::Math::Triangle_ClipAgainstPlane({ 0.0f, -1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, test, sclipped[0], sclipped[1]); break; + case 1: nTrisToAdd = GFX3D::Math::Triangle_ClipAgainstPlane({ 0.0f, +1.0f, 0.0f }, { 0.0f, -1.0f, 0.0f }, test, sclipped[0], sclipped[1]); break; + case 2: nTrisToAdd = GFX3D::Math::Triangle_ClipAgainstPlane({ -1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f }, test, sclipped[0], sclipped[1]); break; + case 3: nTrisToAdd = GFX3D::Math::Triangle_ClipAgainstPlane({ +1.0f, 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f }, test, sclipped[0], sclipped[1]); break; + } + + + // Clipping may yield a variable number of triangles, so + // add these new ones to the back of the queue for subsequent + // clipping against next planes + for (int w = 0; w < nTrisToAdd; w++) + listTriangles.push_back(sclipped[w]); + } + nNewTriangles = listTriangles.size(); + } + + for (auto &triRaster : listTriangles) + { + // Scale to viewport + /*triRaster.p[0].x *= -1.0f; + triRaster.p[1].x *= -1.0f; + triRaster.p[2].x *= -1.0f; + triRaster.p[0].y *= -1.0f; + triRaster.p[1].y *= -1.0f; + triRaster.p[2].y *= -1.0f;*/ + vec3d vOffsetView = { 1,1,0 }; + triRaster.p[0] = Math::Vec_Add(triRaster.p[0], vOffsetView); + triRaster.p[1] = Math::Vec_Add(triRaster.p[1], vOffsetView); + triRaster.p[2] = Math::Vec_Add(triRaster.p[2], vOffsetView); + triRaster.p[0].x *= 0.5f * fViewW; + triRaster.p[0].y *= 0.5f * fViewH; + triRaster.p[1].x *= 0.5f * fViewW; + triRaster.p[1].y *= 0.5f * fViewH; + triRaster.p[2].x *= 0.5f * fViewW; + triRaster.p[2].y *= 0.5f * fViewH; + vOffsetView = { fViewX,fViewY,0 }; + triRaster.p[0] = Math::Vec_Add(triRaster.p[0], vOffsetView); + triRaster.p[1] = Math::Vec_Add(triRaster.p[1], vOffsetView); + triRaster.p[2] = Math::Vec_Add(triRaster.p[2], vOffsetView); + + // For now, just draw triangle + + if (flags & RENDER_TEXTURED) + { + TexturedTriangle( + triRaster.p[0].x, triRaster.p[0].y, triRaster.t[0].x, triRaster.t[0].y, triRaster.t[0].z, + triRaster.p[1].x, triRaster.p[1].y, triRaster.t[1].x, triRaster.t[1].y, triRaster.t[1].z, + triRaster.p[2].x, triRaster.p[2].y, triRaster.t[2].x, triRaster.t[2].y, triRaster.t[2].z, + sprTexture); + } + + if (flags & RENDER_WIRE) + { + DrawTriangleWire(triRaster, olc::RED); + } + + if (flags & RENDER_FLAT) + { + DrawTriangleFlat(triRaster); + } + + nTriangleDrawnCount++; + } + } + } + + return nTriangleDrawnCount; + } +} + +#endif \ No newline at end of file From 6ce41e854fd1e9c0fec8820c0add0f80db76611b Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sat, 19 Jan 2019 23:17:53 +0000 Subject: [PATCH 05/12] Add files via upload --- olcPGEX_Graphics3D.h | 1 + 1 file changed, 1 insertion(+) diff --git a/olcPGEX_Graphics3D.h b/olcPGEX_Graphics3D.h index d3e217a..954e776 100644 --- a/olcPGEX_Graphics3D.h +++ b/olcPGEX_Graphics3D.h @@ -53,6 +53,7 @@ Twitter: https://www.twitter.com/javidx9 Twitch: https://www.twitch.tv/javidx9 GitHub: https://www.github.com/onelonecoder + Patreon: https://www.patreon.com/javidx9 Homepage: https://www.onelonecoder.com Author From a4d0a7c2509182f27aae7e71c1c192cc53249ce5 Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sat, 19 Jan 2019 23:20:57 +0000 Subject: [PATCH 06/12] Added Big Project CCC#1 --- CarCrimeCity/Part1/City_Roads1_mip0.png | Bin 0 -> 42193 bytes .../Part1/OneLoneCoder_CarCrimeCity1.cpp | 670 ++++++ CarCrimeCity/Part1/car_top1.png | Bin 0 -> 16926 bytes CarCrimeCity/Part1/example1.city | Bin 0 -> 32776 bytes CarCrimeCity/Part1/olcPGEX_Graphics3D.h | 1174 ++++++++++ CarCrimeCity/Part1/olcPixelGameEngine.h | 2067 +++++++++++++++++ 6 files changed, 3911 insertions(+) create mode 100644 CarCrimeCity/Part1/City_Roads1_mip0.png create mode 100644 CarCrimeCity/Part1/OneLoneCoder_CarCrimeCity1.cpp create mode 100644 CarCrimeCity/Part1/car_top1.png create mode 100644 CarCrimeCity/Part1/example1.city create mode 100644 CarCrimeCity/Part1/olcPGEX_Graphics3D.h create mode 100644 CarCrimeCity/Part1/olcPixelGameEngine.h diff --git a/CarCrimeCity/Part1/City_Roads1_mip0.png b/CarCrimeCity/Part1/City_Roads1_mip0.png new file mode 100644 index 0000000000000000000000000000000000000000..46cec50ff954a866ba264e4844151b07299b3e2e GIT binary patch literal 42193 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�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 B1+4%8 literal 0 HcmV?d00001 diff --git a/CarCrimeCity/Part1/OneLoneCoder_CarCrimeCity1.cpp b/CarCrimeCity/Part1/OneLoneCoder_CarCrimeCity1.cpp new file mode 100644 index 0000000..8a4c7a7 --- /dev/null +++ b/CarCrimeCity/Part1/OneLoneCoder_CarCrimeCity1.cpp @@ -0,0 +1,670 @@ +/* + BIG PROJECT - Top Down City Based Car Crime Game Part #1 + "Probably gonna regret starting this one..." - javidx9 + + License (OLC-3) + ~~~~~~~~~~~~~~~ + + Copyright 2018-2019 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. + + Instructions: + ~~~~~~~~~~~~~ + This is the source that accompanies part 1 of the video series which + can be viewed via the link below. Here you can create and edit a city + from a top down perspective ad navigate it with the car. + + Using the mouse left click you can select cells. Using right click clears + all the selected cells. + + "E" - Lowers building height + "T" - Raises building height + "R" - Places road + "Z, X" - Zoom + "Up, Left, Right" - Control car + "F5" - Save current city + "F8" - Load existing city + + A default city is provided for you - "example1.city", please ensure + you have this file also. + + Relevant Video: https://youtu.be/mD6b_hP17WI + + 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 + Patreon: https://www.patreon.com/javidx9 + Homepage: https://www.onelonecoder.com + + Author + ~~~~~~ + David Barr, aka javidx9, ©OneLoneCoder 2018 +*/ + +#define OLC_PGE_APPLICATION +#include "olcPixelGameEngine.h" +#include "olcPGEX_Graphics3D.h" + +#include +#include +#include +#include +#include +#include +#include + +// Override base class with your custom functionality +class CarCrimeCity : public olc::PixelGameEngine +{ +public: + CarCrimeCity() + { + sAppName = "Car Crime City"; + } + +private: + + // Define the cell + struct sCell + { + int nHeight = 0; + int nWorldX = 0; + int nWorldY = 0; + bool bRoad = false; + bool bBuilding = true; + }; + + // Map variables + int nMapWidth; + int nMapHeight; + sCell *pMap; + + olc::Sprite *sprAll; + olc::Sprite *sprGround; + olc::Sprite *sprRoof; + olc::Sprite *sprFrontage; + olc::Sprite *sprWindows; + olc::Sprite *sprRoad[12]; + olc::Sprite *sprCar; + + float fCameraX = 0.0f; + float fCameraY = 0.0f; + float fCameraZ = -10.0f; + + olc::GFX3D::mesh meshCube; + olc::GFX3D::mesh meshFlat; + olc::GFX3D::mesh meshWallsOut; + + float fCarAngle = 0.0f; + float fCarSpeed = 2.0f; + olc::GFX3D::vec3d vecCarVel = { 0,0,0 }; + olc::GFX3D::vec3d vecCarPos = { 0,0,0 }; + + + int nMouseWorldX = 0; + int nMouseWorldY = 0; + int nOldMouseWorldX = 0; + int nOldMouseWorldY = 0; + + bool bMouseDown = false; + std::unordered_set setSelectedCells; + + olc::GFX3D::PipeLine pipeRender; + olc::GFX3D::mat4x4 matProj; + olc::GFX3D::vec3d vUp = { 0,1,0 }; + olc::GFX3D::vec3d vEye = { 0,0,-10 }; + olc::GFX3D::vec3d vLookDir = { 0,0,1 }; + + olc::GFX3D::vec3d viewWorldTopLeft, viewWorldBottomRight; + + + void SaveCity(std::string sFilename) + { + std::ofstream file(sFilename, std::ios::out | std::ios::binary); + file.write((char*)&nMapWidth, sizeof(int)); + file.write((char*)&nMapHeight, sizeof(int)); + for (int x = 0; x < nMapWidth; x++) + { + for (int y = 0; y < nMapHeight; y++) + { + file.write((char*)&pMap[y*nMapWidth + x], sizeof(sCell)); + } + } + } + + void LoadCity(std::string sFilename) + { + std::ifstream file(sFilename, std::ios::in | std::ios::binary); + file.read((char*)&nMapWidth, sizeof(int)); + file.read((char*)&nMapHeight, sizeof(int)); + delete[] pMap; + pMap = new sCell[nMapWidth * nMapHeight]; + for (int x = 0; x < nMapWidth; x++) + { + for (int y = 0; y < nMapHeight; y++) + { + file.read((char*)&pMap[y*nMapWidth + x], sizeof(sCell)); + } + } + } + +public: + bool OnUserCreate() override + { + // Load Sprite Sheet + sprAll = new olc::Sprite("City_Roads1_mip0.png"); + + // Here we break up the sprite sheet into individual textures. This is more + // out of convenience than anything else, as it keeps the texture coordinates + // easy to manipulate + + // Building Lowest Floor + sprFrontage = new olc::Sprite(32, 96); + SetDrawTarget(sprFrontage); + DrawPartialSprite(0, 0, sprAll, 288, 64, 32, 96); + + // Building Windows + sprWindows = new olc::Sprite(32, 96); + SetDrawTarget(sprWindows); + DrawPartialSprite(0, 0, sprAll, 320, 64, 32, 96); + + // Plain Grass Field + sprGround = new olc::Sprite(96, 96); + SetDrawTarget(sprGround); + DrawPartialSprite(0, 0, sprAll, 192, 0, 96, 96); + + // Building Roof + sprRoof = new olc::Sprite(96, 96); + SetDrawTarget(sprRoof); + DrawPartialSprite(0, 0, sprAll, 352, 64, 96, 96); + + // There are 12 Road Textures, aranged in a 3x4 grid + for (int r = 0; r < 12; r++) + { + sprRoad[r] = new olc::Sprite(96, 96); + SetDrawTarget(sprRoad[r]); + DrawPartialSprite(0, 0, sprAll, (r%3)*96, (r/3)*96, 96, 96); + } + + // Don't foregt to set the draw target back to being the main screen (been there... wasted 1.5 hours :| ) + SetDrawTarget(nullptr); + + // The Yellow Car + sprCar = new olc::Sprite("car_top.png"); + + + + // Define the city map, a 64x32 array of Cells. Initialise cells + // to be just grass fields + nMapWidth = 64; + nMapHeight = 32; + pMap = new sCell[nMapWidth * nMapHeight]; + for (int x = 0; x < nMapWidth; x++) + { + for (int y = 0; y < nMapHeight; y++) + { + pMap[y*nMapWidth + x].bRoad = false; + pMap[y*nMapWidth + x].nHeight = 0; + pMap[y*nMapWidth + x].nWorldX = x; + pMap[y*nMapWidth + x].nWorldY = y; + } + } + + + // Now we'll hand construct some meshes. These are DELIBERATELY simple and not optimised (see a later video) + // Here the geometry is unit in size (1x1x1) + + // A Full cube - Always useful for debugging + meshCube.tris = + { + // SOUTH + { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }, + { 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + + // EAST + { 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }, + { 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + + // NORTH + { 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }, + { 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + + // WEST + { 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }, + { 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + + // TOP + { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }, + { 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + + // BOTTOM + { 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }, + { 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + + }; + + // A Flat quad + meshFlat.tris = + { + { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + { 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, }, + }; + + // The four outer walls of a cell + meshWallsOut.tris = + { + // EAST + { 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.2f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, }, + { 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.2f, 1.0f, 1.0f, 0.0f, 0.2f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, }, + + // WEST + { 0.0f, 0.0f, 0.2f, 1.0f, 0.0f, 1.0f, 0.2f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }, + { 0.0f, 0.0f, 0.2f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + + // TOP + { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.2f, 1.0f, 1.0f, 1.0f, 0.2f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, }, + { 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.2f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + + // BOTTOM + { 1.0f, 0.0f, 0.2f, 1.0f, 0.0f, 0.0f, 0.2f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }, + { 1.0f, 0.0f, 0.2f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, }, + }; + + + // Initialise the 3D Graphics PGE Extension. This is required + // to setup internal buffers to the same size as the main output + olc::GFX3D::ConfigureDisplay(); + + // Configure the rendering pipeline with projection and viewport properties + pipeRender.SetProjection(90.0f, (float)ScreenHeight() / (float)ScreenWidth(), 0.1f, 1000.0f, 0.0f, 0.0f, ScreenWidth(), ScreenHeight()); + + // Also make a projection matrix, we might need this later + matProj = olc::GFX3D::Math::Mat_MakeProjection(90.0f, (float)ScreenHeight() / (float)ScreenWidth(), 0.1f, 1000.0f); + + LoadCity("example1.city"); + + // Ok, lets go! + return true; + } + + bool OnUserUpdate(float fElapsedTime) override + { + // Directly manipulate camera + //if (GetKey(olc::Key::W).bHeld) fCameraY -= 2.0f * fElapsedTime; + //if (GetKey(olc::Key::S).bHeld) fCameraY += 2.0f * fElapsedTime; + //if (GetKey(olc::Key::A).bHeld) fCameraX -= 2.0f * fElapsedTime; + //if (GetKey(olc::Key::D).bHeld) fCameraX += 2.0f * fElapsedTime; + if (GetKey(olc::Key::Z).bHeld) fCameraZ += 5.0f * fElapsedTime; + if (GetKey(olc::Key::X).bHeld) fCameraZ -= 5.0f * fElapsedTime; + + if (GetKey(olc::Key::F5).bReleased) SaveCity("example1.city"); + if (GetKey(olc::Key::F8).bReleased) LoadCity("example1.city"); + + // === Handle User Input for Editing == + + // If there are no selected cells, then only edit the cell under the current mouse cursor + // otherwise iterate through the set of sleected cells and apply to all of them + + // Check that cell exists in valid 2D map space + if (nMouseWorldX >= 0 && nMouseWorldX < nMapWidth && nMouseWorldY >= 0 && nMouseWorldY < nMapHeight) + { + // Press "R" to toggle Road flag for selected cell(s) + if (GetKey(olc::Key::R).bPressed) + { + if (!setSelectedCells.empty()) + { + for (auto &cell : setSelectedCells) + { + cell->bRoad = !cell->bRoad; + } + } + else + pMap[nMouseWorldY*nMapWidth + nMouseWorldX].bRoad = !pMap[nMouseWorldY*nMapWidth + nMouseWorldX].bRoad; + } + + // Press "T" to increase height for selected cell(s) + if (GetKey(olc::Key::T).bPressed) + { + if (!setSelectedCells.empty()) + { + for (auto &cell : setSelectedCells) + { + cell->nHeight++; + } + } + else + pMap[nMouseWorldY*nMapWidth + nMouseWorldX].nHeight++; + } + + // Press "E" to decrease height for selected cell(s) + if (GetKey(olc::Key::E).bPressed) + { + if (!setSelectedCells.empty()) + { + for (auto &cell : setSelectedCells) + { + cell->nHeight--; + } + } + else + pMap[nMouseWorldY*nMapWidth + nMouseWorldX].nHeight--; + } + } + + + // === Car User Input === + + if (GetKey(olc::Key::LEFT).bHeld) fCarAngle -= 4.0f * fElapsedTime; + if (GetKey(olc::Key::RIGHT).bHeld) fCarAngle += 4.0f * fElapsedTime; + + olc::GFX3D::vec3d a = { 1, 0, 0 }; + olc::GFX3D::mat4x4 m = olc::GFX3D::Math::Mat_MakeRotationZ(fCarAngle); + vecCarVel = olc::GFX3D::Math::Mat_MultiplyVector(m, a); + + if (GetKey(olc::Key::UP).bHeld) + { + vecCarPos.x += vecCarVel.x * fCarSpeed * fElapsedTime; + vecCarPos.y += vecCarVel.y * fCarSpeed * fElapsedTime; + } + + // === Position Camera === + + // Our camera currently follows the car, and the car stays in the middle of + // the screen. We need to know where the camera is before we can work with + // on screen interactions + fCameraY = vecCarPos.y; + fCameraX = vecCarPos.x; + vEye = { fCameraX,fCameraY,fCameraZ }; + olc::GFX3D::vec3d vLookTarget = olc::GFX3D::Math::Vec_Add(vEye, vLookDir); + + // Setup the camera properties for the pipeline - aka "view" transform + pipeRender.SetCamera(vEye, vLookTarget, vUp); + + + // === Calculate Mouse Position on Ground Plane === + + // Here we take the screen coordinate of the mouse, transform it into normalised space (-1 --> +1) + // for both axes. Instead of inverting and multiplying by the projection matrix, we only need the + // aspect ratio parameters, with which we'll scale the mouse coordinate. This new point is then + // multiplied by the inverse of the look at matrix (camera view matrix) aka a point at matrix, to + // transform the new point into world space. + // + // Now, the thing is, a 2D point is no good on its own, our world has depth. If we treat the 2D + // point as a ray cast from (0, 0)->(mx, my), we can see where this ray intersects with a plane + // at a specific depth. + + // Create a point at matrix, if you recall, this is the inverse of the look at matrix + // used by the camera + olc::GFX3D::mat4x4 matView = olc::GFX3D::Math::Mat_PointAt(vEye, vLookTarget, vUp); + + // Assume the origin of the mouse ray is the middle of the screen... + olc::GFX3D::vec3d vecMouseOrigin = { 0.0f, 0.0f, 0.0f }; + + // ...and that a ray is cast to the mouse location from the origin. Here we translate + // the mouse coordinates into viewport coordinates + olc::GFX3D::vec3d vecMouseDir = { + 2.0f * ((GetMouseX() / (float)ScreenWidth()) - 0.5f) / matProj.m[0][0], + 2.0f * ((GetMouseY() / (float)ScreenHeight()) - 0.5f) / matProj.m[1][1], + 1.0f, + 0.0f }; + + // Now transform the origin point and ray direction by the inverse of the camera + vecMouseOrigin = olc::GFX3D::Math::Mat_MultiplyVector(matView, vecMouseOrigin); + vecMouseDir = olc::GFX3D::Math::Mat_MultiplyVector(matView, vecMouseDir); + + // Extend the mouse ray to a large length + vecMouseDir = olc::GFX3D::Math::Vec_Mul(vecMouseDir, 1000.0f); + + // Offset the mouse ray by the mouse origin + vecMouseDir = olc::GFX3D::Math::Vec_Add(vecMouseOrigin, vecMouseDir); + + // All of our intersections for mouse checks occur in the ground plane (z=0), so + // define a plane at that location + olc::GFX3D::vec3d plane_p = { 0.0f, 0.0f, 0.0f }; + olc::GFX3D::vec3d plane_n = { 0.0f, 0.0f, 1.0f }; + + // Calculate Mouse Location in plane, by doing a line/plane intersection test + float t = 0.0f; + olc::GFX3D::vec3d mouse3d = olc::GFX3D::Math::Vec_IntersectPlane(plane_p, plane_n, vecMouseOrigin, vecMouseDir, t); + + + + // === Now we have the mouse in 3D! Handle mouse user input === + + // Left click & left click drag selects cells by adding them to the set of selected cells + // Here I make sure only to do this if the cell under the mouse has changed from the + // previous frame, but the set will also reject duplicate cells being added + if (GetMouse(0).bHeld && ((nMouseWorldX != nOldMouseWorldX) || (nMouseWorldY != nOldMouseWorldY))) + setSelectedCells.emplace(&pMap[nMouseWorldY * nMapWidth + nMouseWorldX]); + + // Single clicking cells also adds them + if (GetMouse(0).bPressed) + setSelectedCells.emplace(&pMap[nMouseWorldY * nMapWidth + nMouseWorldX]); + + // If the user right clicks, the set of selected cells is emptied + if (GetMouse(1).bReleased) + setSelectedCells.clear(); + + // Cache the current mouse position to use during comparison in next frame + nOldMouseWorldX = nMouseWorldX; + nOldMouseWorldY = nMouseWorldY; + + nMouseWorldX = (int)mouse3d.x; + nMouseWorldY = (int)mouse3d.y; + + + + + // === Rendering === + + // Right, now we're gonna render the scene! + + // First Clear the screen and the depth buffer + Clear(olc::BLUE); + olc::GFX3D::ClearDepth(); + + // === Calculate Visible World === + + // Since we now have the transforms to convert screen space into ground plane space, we + // can calculate the visible extents of the world, regardless of zoom level! The method is + // exactly the same for the mouse, but we use fixed screen coordinates that represent the + // top left, and bottom right of the screen + + // Work out Top Left Ground Cell + vecMouseDir = { -1.0f / matProj.m[0][0],-1.0f / matProj.m[1][1], 1.0f, 0.0f }; + vecMouseDir = olc::GFX3D::Math::Mat_MultiplyVector(matView, vecMouseDir); + vecMouseDir = olc::GFX3D::Math::Vec_Mul(vecMouseDir, 1000.0f); + vecMouseDir = olc::GFX3D::Math::Vec_Add(vecMouseOrigin, vecMouseDir); + viewWorldTopLeft = olc::GFX3D::Math::Vec_IntersectPlane(plane_p, plane_n, vecMouseOrigin, vecMouseDir, t); + + // Work out Bottom Right Ground Cell + vecMouseDir = { 1.0f / matProj.m[0][0], 1.0f / matProj.m[1][1], 1.0f, 0.0f }; + vecMouseDir = olc::GFX3D::Math::Mat_MultiplyVector(matView, vecMouseDir); + vecMouseDir = olc::GFX3D::Math::Vec_Mul(vecMouseDir, 1000.0f); + vecMouseDir = olc::GFX3D::Math::Vec_Add(vecMouseOrigin, vecMouseDir); + viewWorldBottomRight = olc::GFX3D::Math::Vec_IntersectPlane(plane_p, plane_n, vecMouseOrigin, vecMouseDir, t); + + // Calculate visible tiles + //int nStartX = 0; + //int nEndX = nMapWidth; + //int nStartY = 0; + //int nEndY = nMapHeight; + + int nStartX = std::max(0, (int)viewWorldTopLeft.x - 1); + int nEndX = std::min(nMapWidth, (int)viewWorldBottomRight.x + 1); + int nStartY = std::max(0, (int)viewWorldTopLeft.y - 1); + int nEndY = std::min(nMapHeight, (int)viewWorldBottomRight.y + 1); + + + // Iterate through all the cells we wish to draw. Each cell is 1x1 and elevates in the Z -Axis + for (int x = nStartX; x < nEndX; x++) + { + for (int y = nStartY; y < nEndY; y++) + { + if (pMap[y*nMapWidth + x].bRoad) + { + // Cell is a road, look at neighbouring cells. If they are roads also, + // then choose the appropriate texture that joins up correctly + + int road = 0; + auto r = [&](int i, int j) + { + return pMap[(y + j) * nMapWidth + (x + i)].bRoad; + }; + + if (r(0, -1) && r(0, +1) && !r(-1, 0) && !r(+1, 0)) road = 0; + if (!r(0, -1) && !r(0, +1) && r(-1, 0) && r(+1, 0)) road = 1; + + if (!r(0, -1) && r(0, +1) && !r(-1, 0) && r(+1, 0)) road = 3; + if (!r(0, -1) && r(0, +1) && r(-1, 0) && r(+1, 0)) road = 4; + if (!r(0, -1) && r(0, +1) && r(-1, 0) && !r(+1, 0)) road = 5; + + if (r(0, -1) && r(0, +1) && !r(-1, 0) && r(+1, 0)) road = 6; + if (r(0, -1) && r(0, +1) && r(-1, 0) && r(+1, 0)) road = 7; + if (r(0, -1) && r(0, +1) && r(-1, 0) && !r(+1, 0)) road = 8; + + if (r(0, -1) && !r(0, +1) && !r(-1, 0) && r(+1, 0)) road = 9; + if (r(0, -1) && !r(0, +1) && r(-1, 0) && r(+1, 0)) road = 10; + if (r(0, -1) && !r(0, +1) && r(-1, 0) && !r(+1, 0)) road = 11; + + // Create a translation transform to position the cell in the world + olc::GFX3D::mat4x4 matWorld = olc::GFX3D::Math::Mat_MakeTranslation(x, y, 0.0f); + pipeRender.SetTransform(matWorld); + + // Set the appropriate texture to use + pipeRender.SetTexture(sprRoad[road]); + + // Draw a flat quad + pipeRender.Render(meshFlat.tris); + + } + else // Not Road + { + // If the cell is not considered road, then draw it appropriately + + if (pMap[y*nMapWidth + x].nHeight < 0) + { + // Cell is blank - for now ;-P + } + + if (pMap[y*nMapWidth + x].nHeight == 0) + { + // Cell is ground, draw a flat grass quad at height 0 + olc::GFX3D::mat4x4 matWorld = olc::GFX3D::Math::Mat_MakeTranslation(x, y, 0.0f); + pipeRender.SetTransform(matWorld); + pipeRender.SetTexture(sprGround); + pipeRender.Render(meshFlat.tris); + } + + if (pMap[y*nMapWidth + x].nHeight > 0) + { + // Cell is Building, for now, we'll draw each storey as a seperate mesh + int h, t; + t = pMap[y*nMapWidth + x].nHeight; + + for (h = 0; h < t; h++) + { + // Create a transform that positions the storey according to its height + olc::GFX3D::mat4x4 matWorld = olc::GFX3D::Math::Mat_MakeTranslation(x, y, -(h + 1) * 0.2f); + pipeRender.SetTransform(matWorld); + + // Choose a texture, if its ground level, use the "street level front", otherwise use windows + pipeRender.SetTexture(h == 0 ? sprFrontage : sprWindows); + pipeRender.Render(meshWallsOut.tris); + } + + // Top the building off with a roof + olc::GFX3D::mat4x4 matWorld = olc::GFX3D::Math::Mat_MakeTranslation(x, y, -(h) * 0.2f); + pipeRender.SetTransform(matWorld); + pipeRender.SetTexture(sprRoof); + pipeRender.Render(meshFlat.tris); + } + } + } + } + + // Draw Selected Cells, iterate through the set of cells, and draw a wireframe quad at ground level + // to indicate it is in the selection set + for (auto &cell : setSelectedCells) + { + // Draw CursorCube + olc::GFX3D::mat4x4 matWorld = olc::GFX3D::Math::Mat_MakeTranslation(cell->nWorldX, cell->nWorldY, 0.0f); + pipeRender.SetTransform(matWorld); + pipeRender.SetTexture(sprRoof); + pipeRender.Render(meshFlat.tris, olc::GFX3D::RENDER_WIRE); + } + + // Draw Car, a few transforms required for this + + // 1) Offset the car to the middle of the quad + olc::GFX3D::mat4x4 matCarOffset = olc::GFX3D::Math::Mat_MakeTranslation(-0.5f, -0.5f, -0.0f); + // 2) The quad is currently unit square, scale it to be more rectangular and smaller than the cells + olc::GFX3D::mat4x4 matCarScale = olc::GFX3D::Math::Mat_MakeScale(0.4f, 0.2f, 1.0f); + // 3) Combine into matrix + olc::GFX3D::mat4x4 matCar = olc::GFX3D::Math::Mat_MultiplyMatrix(matCarOffset, matCarScale); + // 4) Rotate the car around its offset origin, according to its angle + olc::GFX3D::mat4x4 matCarRot = olc::GFX3D::Math::Mat_MakeRotationZ(fCarAngle); + matCar = olc::GFX3D::Math::Mat_MultiplyMatrix(matCar, matCarRot); + // 5) Translate the car into its position in the world. Give it a little elevation so its baove the ground + olc::GFX3D::mat4x4 matCarTrans = olc::GFX3D::Math::Mat_MakeTranslation(vecCarPos.x, vecCarPos.y, -0.01f); + matCar = olc::GFX3D::Math::Mat_MultiplyMatrix(matCar, matCarTrans); + + // Set the car texture to the pipeline + pipeRender.SetTexture(sprCar); + // Apply "world" transform to pipeline + pipeRender.SetTransform(matCar); + + // The car has transparency, so enable it + SetPixelMode(olc::Pixel::ALPHA); + // Render the quad + pipeRender.Render(meshFlat.tris); + // Set transparency back to none to optimise drawing other pixels + SetPixelMode(olc::Pixel::NORMAL); + + + // Draw the current camera position for debug information + //DrawString(10, 30, "CX: " + std::to_string(fCameraX) + " CY: " + std::to_string(fCameraY) + " CZ: " + std::to_string(fCameraZ)); + return true; + } +}; + + + +int main() +{ + CarCrimeCity demo; + if (demo.Construct(768, 480, 2, 2)) + demo.Start(); + return 0; +} \ No newline at end of file diff --git a/CarCrimeCity/Part1/car_top1.png b/CarCrimeCity/Part1/car_top1.png new file mode 100644 index 0000000000000000000000000000000000000000..15ceb1d9684af85408c87d1e1398293763c240d8 GIT binary patch literal 16926 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 literal 0 HcmV?d00001 diff --git a/CarCrimeCity/Part1/example1.city b/CarCrimeCity/Part1/example1.city new file mode 100644 index 0000000000000000000000000000000000000000..47dfb0ba41cc1f2391547cbf3aab1556f6f50830 GIT binary patch literal 32776 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 KAAjTAp8vlH?JM*E literal 0 HcmV?d00001 diff --git a/CarCrimeCity/Part1/olcPGEX_Graphics3D.h b/CarCrimeCity/Part1/olcPGEX_Graphics3D.h new file mode 100644 index 0000000..954e776 --- /dev/null +++ b/CarCrimeCity/Part1/olcPGEX_Graphics3D.h @@ -0,0 +1,1174 @@ +/* + olcPGEX_Graphics3D.h + + +-------------------------------------------------------------+ + | OneLoneCoder Pixel Game Engine Extension | + | 3D Rendering - v0.1 | + +-------------------------------------------------------------+ + + What is this? + ~~~~~~~~~~~~~ + This is an extension to the olcPixelGameEngine, which provides + support for software rendering 3D graphics. + + NOTE!!! This file is under development and may change! + + License (OLC-3) + ~~~~~~~~~~~~~~~ + + Copyright 2018-2019 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 + Patreon: https://www.patreon.com/javidx9 + Homepage: https://www.onelonecoder.com + + Author + ~~~~~~ + David Barr, aka javidx9, ©OneLoneCoder 2018 +*/ + + +#ifndef OLC_PGEX_GFX3D +#define OLC_PGEX_GFX3D + +#include +#include +#include +#undef min +#undef max + +namespace olc +{ + // Container class for Advanced 2D Drawing functions + class GFX3D : public olc::PGEX + { + + public: + + struct vec2d + { + float x = 0; + float y = 0; + float z = 0; + }; + + struct vec3d + { + float x = 0; + float y = 0; + float z = 0; + float w = 1; // Need a 4th term to perform sensible matrix vector multiplication + }; + + struct triangle + { + vec3d p[3]; + vec2d t[3]; + olc::Pixel col; + }; + + struct mat4x4 + { + float m[4][4] = { 0 }; + }; + + struct mesh + { + std::vector tris; + }; + + class Math + { + public: + inline Math(); + public: + inline static vec3d Mat_MultiplyVector(mat4x4 &m, vec3d &i); + inline static mat4x4 Mat_MultiplyMatrix(mat4x4 &m1, mat4x4 &m2); + inline static mat4x4 Mat_MakeIdentity(); + inline static mat4x4 Mat_MakeRotationX(float fAngleRad); + inline static mat4x4 Mat_MakeRotationY(float fAngleRad); + inline static mat4x4 Mat_MakeRotationZ(float fAngleRad); + inline static mat4x4 Mat_MakeScale(float x, float y, float z); + inline static mat4x4 Mat_MakeTranslation(float x, float y, float z); + inline static mat4x4 Mat_MakeProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar); + inline static mat4x4 Mat_PointAt(vec3d &pos, vec3d &target, vec3d &up); + inline static mat4x4 Mat_QuickInverse(mat4x4 &m); // Only for Rotation/Translation Matrices + inline static mat4x4 Mat_Inverse(olc::GFX3D::mat4x4 &m); + + inline static vec3d Vec_Add(vec3d &v1, vec3d &v2); + inline static vec3d Vec_Sub(vec3d &v1, vec3d &v2); + inline static vec3d Vec_Mul(vec3d &v1, float k); + inline static vec3d Vec_Div(vec3d &v1, float k); + inline static float Vec_DotProduct(vec3d &v1, vec3d &v2); + inline static float Vec_Length(vec3d &v); + inline static vec3d Vec_Normalise(vec3d &v); + inline static vec3d Vec_CrossProduct(vec3d &v1, vec3d &v2); + inline static vec3d Vec_IntersectPlane(vec3d &plane_p, vec3d &plane_n, vec3d &lineStart, vec3d &lineEnd, float &t); + + inline static int Triangle_ClipAgainstPlane(vec3d plane_p, vec3d plane_n, triangle &in_tri, triangle &out_tri1, triangle &out_tri2); + }; + + enum RENDERFLAGS + { + RENDER_WIRE = 0x01, + RENDER_FLAT = 0x02, + RENDER_TEXTURED = 0x04, + RENDER_CULL_CW = 0x08, + RENDER_CULL_CCW = 0x10, + RENDER_DEPTH = 0x20, + }; + + + class PipeLine + { + public: + PipeLine(); + + public: + void SetProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar, float fLeft, float fTop, float fWidth, float fHeight); + void SetCamera(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &lookat, olc::GFX3D::vec3d &up); + void SetTransform(olc::GFX3D::mat4x4 &transform); + void SetTexture(olc::Sprite *texture); + void SetLightSource(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &dir, olc::Pixel &col); + uint32_t Render(std::vector &triangles, uint32_t flags = RENDER_CULL_CW | RENDER_TEXTURED | RENDER_DEPTH); + + private: + olc::GFX3D::mat4x4 matProj; + olc::GFX3D::mat4x4 matView; + olc::GFX3D::mat4x4 matWorld; + olc::Sprite *sprTexture; + float fViewX; + float fViewY; + float fViewW; + float fViewH; + }; + + + + public: + //static const int RF_TEXTURE = 0x00000001; + //static const int RF_ = 0x00000002; + + inline static void ConfigureDisplay(); + inline static void ClearDepth(); + inline static void AddTriangleToScene(olc::GFX3D::triangle &tri); + inline static void RenderScene(); + + inline static void DrawTriangleFlat(olc::GFX3D::triangle &tri); + inline static void DrawTriangleWire(olc::GFX3D::triangle &tri, olc::Pixel col = olc::WHITE); + inline static void DrawTriangleTex(olc::GFX3D::triangle &tri, olc::Sprite* spr); + inline static void TexturedTriangle(int x1, int y1, float u1, float v1, float w1, + int x2, int y2, float u2, float v2, float w2, + int x3, int y3, float u3, float v3, float w3, olc::Sprite* spr); + + // Draws a sprite with the transform applied + //inline static void DrawSprite(olc::Sprite *sprite, olc::GFX2D::Transform2D &transform); + + private: + static float* m_DepthBuffer; + }; +} + + + + +namespace olc +{ + olc::GFX3D::Math::Math() + { + + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Mat_MultiplyVector(olc::GFX3D::mat4x4 &m, olc::GFX3D::vec3d &i) + { + vec3d v; + v.x = i.x * m.m[0][0] + i.y * m.m[1][0] + i.z * m.m[2][0] + i.w * m.m[3][0]; + v.y = i.x * m.m[0][1] + i.y * m.m[1][1] + i.z * m.m[2][1] + i.w * m.m[3][1]; + v.z = i.x * m.m[0][2] + i.y * m.m[1][2] + i.z * m.m[2][2] + i.w * m.m[3][2]; + v.w = i.x * m.m[0][3] + i.y * m.m[1][3] + i.z * m.m[2][3] + i.w * m.m[3][3]; + return v; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeIdentity() + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = 1.0f; + matrix.m[1][1] = 1.0f; + matrix.m[2][2] = 1.0f; + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeRotationX(float fAngleRad) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = 1.0f; + matrix.m[1][1] = cosf(fAngleRad); + matrix.m[1][2] = sinf(fAngleRad); + matrix.m[2][1] = -sinf(fAngleRad); + matrix.m[2][2] = cosf(fAngleRad); + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeRotationY(float fAngleRad) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = cosf(fAngleRad); + matrix.m[0][2] = sinf(fAngleRad); + matrix.m[2][0] = -sinf(fAngleRad); + matrix.m[1][1] = 1.0f; + matrix.m[2][2] = cosf(fAngleRad); + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeRotationZ(float fAngleRad) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = cosf(fAngleRad); + matrix.m[0][1] = sinf(fAngleRad); + matrix.m[1][0] = -sinf(fAngleRad); + matrix.m[1][1] = cosf(fAngleRad); + matrix.m[2][2] = 1.0f; + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeScale(float x, float y, float z) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = x; + matrix.m[1][1] = y; + matrix.m[2][2] = z; + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeTranslation(float x, float y, float z) + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = 1.0f; + matrix.m[1][1] = 1.0f; + matrix.m[2][2] = 1.0f; + matrix.m[3][3] = 1.0f; + matrix.m[3][0] = x; + matrix.m[3][1] = y; + matrix.m[3][2] = z; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MakeProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar) + { + float fFovRad = 1.0f / tanf(fFovDegrees * 0.5f / 180.0f * 3.14159f); + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = fAspectRatio * fFovRad; + matrix.m[1][1] = fFovRad; + matrix.m[2][2] = fFar / (fFar - fNear); + matrix.m[3][2] = (-fFar * fNear) / (fFar - fNear); + matrix.m[2][3] = 1.0f; + matrix.m[3][3] = 0.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_MultiplyMatrix(olc::GFX3D::mat4x4 &m1, olc::GFX3D::mat4x4 &m2) + { + olc::GFX3D::mat4x4 matrix; + for (int c = 0; c < 4; c++) + for (int r = 0; r < 4; r++) + matrix.m[r][c] = m1.m[r][0] * m2.m[0][c] + m1.m[r][1] * m2.m[1][c] + m1.m[r][2] * m2.m[2][c] + m1.m[r][3] * m2.m[3][c]; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_PointAt(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &target, olc::GFX3D::vec3d &up) + { + // Calculate new forward direction + olc::GFX3D::vec3d newForward = Vec_Sub(target, pos); + newForward = Vec_Normalise(newForward); + + // Calculate new Up direction + olc::GFX3D::vec3d a = Vec_Mul(newForward, Vec_DotProduct(up, newForward)); + olc::GFX3D::vec3d newUp = Vec_Sub(up, a); + newUp = Vec_Normalise(newUp); + + // New Right direction is easy, its just cross product + olc::GFX3D::vec3d newRight = Vec_CrossProduct(newUp, newForward); + + // Construct Dimensioning and Translation Matrix + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = newRight.x; matrix.m[0][1] = newRight.y; matrix.m[0][2] = newRight.z; matrix.m[0][3] = 0.0f; + matrix.m[1][0] = newUp.x; matrix.m[1][1] = newUp.y; matrix.m[1][2] = newUp.z; matrix.m[1][3] = 0.0f; + matrix.m[2][0] = newForward.x; matrix.m[2][1] = newForward.y; matrix.m[2][2] = newForward.z; matrix.m[2][3] = 0.0f; + matrix.m[3][0] = pos.x; matrix.m[3][1] = pos.y; matrix.m[3][2] = pos.z; matrix.m[3][3] = 1.0f; + return matrix; + + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_QuickInverse(olc::GFX3D::mat4x4 &m) // Only for Rotation/Translation Matrices + { + olc::GFX3D::mat4x4 matrix; + matrix.m[0][0] = m.m[0][0]; matrix.m[0][1] = m.m[1][0]; matrix.m[0][2] = m.m[2][0]; matrix.m[0][3] = 0.0f; + matrix.m[1][0] = m.m[0][1]; matrix.m[1][1] = m.m[1][1]; matrix.m[1][2] = m.m[2][1]; matrix.m[1][3] = 0.0f; + matrix.m[2][0] = m.m[0][2]; matrix.m[2][1] = m.m[1][2]; matrix.m[2][2] = m.m[2][2]; matrix.m[2][3] = 0.0f; + matrix.m[3][0] = -(m.m[3][0] * matrix.m[0][0] + m.m[3][1] * matrix.m[1][0] + m.m[3][2] * matrix.m[2][0]); + matrix.m[3][1] = -(m.m[3][0] * matrix.m[0][1] + m.m[3][1] * matrix.m[1][1] + m.m[3][2] * matrix.m[2][1]); + matrix.m[3][2] = -(m.m[3][0] * matrix.m[0][2] + m.m[3][1] * matrix.m[1][2] + m.m[3][2] * matrix.m[2][2]); + matrix.m[3][3] = 1.0f; + return matrix; + } + + olc::GFX3D::mat4x4 olc::GFX3D::Math::Mat_Inverse(olc::GFX3D::mat4x4 &m) + { + double det; + + + mat4x4 matInv; + + matInv.m[0][0] = m.m[1][1] * m.m[2][2] * m.m[3][3] - m.m[1][1] * m.m[2][3] * m.m[3][2] - m.m[2][1] * m.m[1][2] * m.m[3][3] + m.m[2][1] * m.m[1][3] * m.m[3][2] + m.m[3][1] * m.m[1][2] * m.m[2][3] - m.m[3][1] * m.m[1][3] * m.m[2][2]; + matInv.m[1][0] = -m.m[1][0] * m.m[2][2] * m.m[3][3] + m.m[1][0] * m.m[2][3] * m.m[3][2] + m.m[2][0] * m.m[1][2] * m.m[3][3] - m.m[2][0] * m.m[1][3] * m.m[3][2] - m.m[3][0] * m.m[1][2] * m.m[2][3] + m.m[3][0] * m.m[1][3] * m.m[2][2]; + matInv.m[2][0] = m.m[1][0] * m.m[2][1] * m.m[3][3] - m.m[1][0] * m.m[2][3] * m.m[3][1] - m.m[2][0] * m.m[1][1] * m.m[3][3] + m.m[2][0] * m.m[1][3] * m.m[3][1] + m.m[3][0] * m.m[1][1] * m.m[2][3] - m.m[3][0] * m.m[1][3] * m.m[2][1]; + matInv.m[3][0] = -m.m[1][0] * m.m[2][1] * m.m[3][2] + m.m[1][0] * m.m[2][2] * m.m[3][1] + m.m[2][0] * m.m[1][1] * m.m[3][2] - m.m[2][0] * m.m[1][2] * m.m[3][1] - m.m[3][0] * m.m[1][1] * m.m[2][2] + m.m[3][0] * m.m[1][2] * m.m[2][1]; + matInv.m[0][1] = -m.m[0][1] * m.m[2][2] * m.m[3][3] + m.m[0][1] * m.m[2][3] * m.m[3][2] + m.m[2][1] * m.m[0][2] * m.m[3][3] - m.m[2][1] * m.m[0][3] * m.m[3][2] - m.m[3][1] * m.m[0][2] * m.m[2][3] + m.m[3][1] * m.m[0][3] * m.m[2][2]; + matInv.m[1][1] = m.m[0][0] * m.m[2][2] * m.m[3][3] - m.m[0][0] * m.m[2][3] * m.m[3][2] - m.m[2][0] * m.m[0][2] * m.m[3][3] + m.m[2][0] * m.m[0][3] * m.m[3][2] + m.m[3][0] * m.m[0][2] * m.m[2][3] - m.m[3][0] * m.m[0][3] * m.m[2][2]; + matInv.m[2][1] = -m.m[0][0] * m.m[2][1] * m.m[3][3] + m.m[0][0] * m.m[2][3] * m.m[3][1] + m.m[2][0] * m.m[0][1] * m.m[3][3] - m.m[2][0] * m.m[0][3] * m.m[3][1] - m.m[3][0] * m.m[0][1] * m.m[2][3] + m.m[3][0] * m.m[0][3] * m.m[2][1]; + matInv.m[3][1] = m.m[0][0] * m.m[2][1] * m.m[3][2] - m.m[0][0] * m.m[2][2] * m.m[3][1] - m.m[2][0] * m.m[0][1] * m.m[3][2] + m.m[2][0] * m.m[0][2] * m.m[3][1] + m.m[3][0] * m.m[0][1] * m.m[2][2] - m.m[3][0] * m.m[0][2] * m.m[2][1]; + matInv.m[0][2] = m.m[0][1] * m.m[1][2] * m.m[3][3] - m.m[0][1] * m.m[1][3] * m.m[3][2] - m.m[1][1] * m.m[0][2] * m.m[3][3] + m.m[1][1] * m.m[0][3] * m.m[3][2] + m.m[3][1] * m.m[0][2] * m.m[1][3] - m.m[3][1] * m.m[0][3] * m.m[1][2]; + matInv.m[1][2] = -m.m[0][0] * m.m[1][2] * m.m[3][3] + m.m[0][0] * m.m[1][3] * m.m[3][2] + m.m[1][0] * m.m[0][2] * m.m[3][3] - m.m[1][0] * m.m[0][3] * m.m[3][2] - m.m[3][0] * m.m[0][2] * m.m[1][3] + m.m[3][0] * m.m[0][3] * m.m[1][2]; + matInv.m[2][2] = m.m[0][0] * m.m[1][1] * m.m[3][3] - m.m[0][0] * m.m[1][3] * m.m[3][1] - m.m[1][0] * m.m[0][1] * m.m[3][3] + m.m[1][0] * m.m[0][3] * m.m[3][1] + m.m[3][0] * m.m[0][1] * m.m[1][3] - m.m[3][0] * m.m[0][3] * m.m[1][1]; + matInv.m[3][2] = -m.m[0][0] * m.m[1][1] * m.m[3][2] + m.m[0][0] * m.m[1][2] * m.m[3][1] + m.m[1][0] * m.m[0][1] * m.m[3][2] - m.m[1][0] * m.m[0][2] * m.m[3][1] - m.m[3][0] * m.m[0][1] * m.m[1][2] + m.m[3][0] * m.m[0][2] * m.m[1][1]; + matInv.m[0][3] = -m.m[0][1] * m.m[1][2] * m.m[2][3] + m.m[0][1] * m.m[1][3] * m.m[2][2] + m.m[1][1] * m.m[0][2] * m.m[2][3] - m.m[1][1] * m.m[0][3] * m.m[2][2] - m.m[2][1] * m.m[0][2] * m.m[1][3] + m.m[2][1] * m.m[0][3] * m.m[1][2]; + matInv.m[1][3] = m.m[0][0] * m.m[1][2] * m.m[2][3] - m.m[0][0] * m.m[1][3] * m.m[2][2] - m.m[1][0] * m.m[0][2] * m.m[2][3] + m.m[1][0] * m.m[0][3] * m.m[2][2] + m.m[2][0] * m.m[0][2] * m.m[1][3] - m.m[2][0] * m.m[0][3] * m.m[1][2]; + matInv.m[2][3] = -m.m[0][0] * m.m[1][1] * m.m[2][3] + m.m[0][0] * m.m[1][3] * m.m[2][1] + m.m[1][0] * m.m[0][1] * m.m[2][3] - m.m[1][0] * m.m[0][3] * m.m[2][1] - m.m[2][0] * m.m[0][1] * m.m[1][3] + m.m[2][0] * m.m[0][3] * m.m[1][1]; + matInv.m[3][3] = m.m[0][0] * m.m[1][1] * m.m[2][2] - m.m[0][0] * m.m[1][2] * m.m[2][1] - m.m[1][0] * m.m[0][1] * m.m[2][2] + m.m[1][0] * m.m[0][2] * m.m[2][1] + m.m[2][0] * m.m[0][1] * m.m[1][2] - m.m[2][0] * m.m[0][2] * m.m[1][1]; + + det = m.m[0][0] * matInv.m[0][0] + m.m[0][1] * matInv.m[1][0] + m.m[0][2] * matInv.m[2][0] + m.m[0][3] * matInv.m[3][0]; + // if (det == 0) return false; + + det = 1.0 / det; + + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + matInv.m[i][j] *= (float)det; + + return matInv; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Add(olc::GFX3D::vec3d &v1, olc::GFX3D::vec3d &v2) + { + return { v1.x + v2.x, v1.y + v2.y, v1.z + v2.z }; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Sub(olc::GFX3D::vec3d &v1, olc::GFX3D::vec3d &v2) + { + return { v1.x - v2.x, v1.y - v2.y, v1.z - v2.z }; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Mul(olc::GFX3D::vec3d &v1, float k) + { + return { v1.x * k, v1.y * k, v1.z * k }; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Div(olc::GFX3D::vec3d &v1, float k) + { + return { v1.x / k, v1.y / k, v1.z / k }; + } + + float olc::GFX3D::Math::Vec_DotProduct(olc::GFX3D::vec3d &v1, olc::GFX3D::vec3d &v2) + { + return v1.x*v2.x + v1.y*v2.y + v1.z * v2.z; + } + + float olc::GFX3D::Math::Vec_Length(olc::GFX3D::vec3d &v) + { + return sqrtf(Vec_DotProduct(v, v)); + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_Normalise(olc::GFX3D::vec3d &v) + { + float l = Vec_Length(v); + return { v.x / l, v.y / l, v.z / l }; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_CrossProduct(olc::GFX3D::vec3d &v1, olc::GFX3D::vec3d &v2) + { + vec3d v; + v.x = v1.y * v2.z - v1.z * v2.y; + v.y = v1.z * v2.x - v1.x * v2.z; + v.z = v1.x * v2.y - v1.y * v2.x; + return v; + } + + olc::GFX3D::vec3d olc::GFX3D::Math::Vec_IntersectPlane(olc::GFX3D::vec3d &plane_p, olc::GFX3D::vec3d &plane_n, olc::GFX3D::vec3d &lineStart, olc::GFX3D::vec3d &lineEnd, float &t) + { + plane_n = Vec_Normalise(plane_n); + float plane_d = -Vec_DotProduct(plane_n, plane_p); + float ad = Vec_DotProduct(lineStart, plane_n); + float bd = Vec_DotProduct(lineEnd, plane_n); + t = (-plane_d - ad) / (bd - ad); + olc::GFX3D::vec3d lineStartToEnd = Vec_Sub(lineEnd, lineStart); + olc::GFX3D::vec3d lineToIntersect = Vec_Mul(lineStartToEnd, t); + return Vec_Add(lineStart, lineToIntersect); + } + + + int olc::GFX3D::Math::Triangle_ClipAgainstPlane(vec3d plane_p, vec3d plane_n, triangle &in_tri, triangle &out_tri1, triangle &out_tri2) + { + // Make sure plane normal is indeed normal + plane_n = Math::Vec_Normalise(plane_n); + + out_tri1.t[0] = in_tri.t[0]; + out_tri2.t[0] = in_tri.t[0]; + out_tri1.t[1] = in_tri.t[1]; + out_tri2.t[1] = in_tri.t[1]; + out_tri1.t[2] = in_tri.t[2]; + out_tri2.t[2] = in_tri.t[2]; + + // Return signed shortest distance from point to plane, plane normal must be normalised + auto dist = [&](vec3d &p) + { + vec3d n = Math::Vec_Normalise(p); + return (plane_n.x * p.x + plane_n.y * p.y + plane_n.z * p.z - Math::Vec_DotProduct(plane_n, plane_p)); + }; + + // Create two temporary storage arrays to classify points either side of plane + // If distance sign is positive, point lies on "inside" of plane + vec3d* inside_points[3]; int nInsidePointCount = 0; + vec3d* outside_points[3]; int nOutsidePointCount = 0; + vec2d* inside_tex[3]; int nInsideTexCount = 0; + vec2d* outside_tex[3]; int nOutsideTexCount = 0; + + + // Get signed distance of each point in triangle to plane + float d0 = dist(in_tri.p[0]); + float d1 = dist(in_tri.p[1]); + float d2 = dist(in_tri.p[2]); + + if (d0 >= 0) { inside_points[nInsidePointCount++] = &in_tri.p[0]; inside_tex[nInsideTexCount++] = &in_tri.t[0]; } + else { + outside_points[nOutsidePointCount++] = &in_tri.p[0]; outside_tex[nOutsideTexCount++] = &in_tri.t[0]; + } + if (d1 >= 0) { + inside_points[nInsidePointCount++] = &in_tri.p[1]; inside_tex[nInsideTexCount++] = &in_tri.t[1]; + } + else { + outside_points[nOutsidePointCount++] = &in_tri.p[1]; outside_tex[nOutsideTexCount++] = &in_tri.t[1]; + } + if (d2 >= 0) { + inside_points[nInsidePointCount++] = &in_tri.p[2]; inside_tex[nInsideTexCount++] = &in_tri.t[2]; + } + else { + outside_points[nOutsidePointCount++] = &in_tri.p[2]; outside_tex[nOutsideTexCount++] = &in_tri.t[2]; + } + + // Now classify triangle points, and break the input triangle into + // smaller output triangles if required. There are four possible + // outcomes... + + if (nInsidePointCount == 0) + { + // All points lie on the outside of plane, so clip whole triangle + // It ceases to exist + + return 0; // No returned triangles are valid + } + + if (nInsidePointCount == 3) + { + // All points lie on the inside of plane, so do nothing + // and allow the triangle to simply pass through + out_tri1 = in_tri; + + return 1; // Just the one returned original triangle is valid + } + + if (nInsidePointCount == 1 && nOutsidePointCount == 2) + { + // Triangle should be clipped. As two points lie outside + // the plane, the triangle simply becomes a smaller triangle + + // Copy appearance info to new triangle + out_tri1.col = olc::MAGENTA;// in_tri.col; + + // The inside point is valid, so keep that... + out_tri1.p[0] = *inside_points[0]; + out_tri1.t[0] = *inside_tex[0]; + + // but the two new points are at the locations where the + // original sides of the triangle (lines) intersect with the plane + float t; + out_tri1.p[1] = Math::Vec_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[0], t); + out_tri1.t[1].x = t * (outside_tex[0]->x - inside_tex[0]->x) + inside_tex[0]->x; + out_tri1.t[1].y = t * (outside_tex[0]->y - inside_tex[0]->y) + inside_tex[0]->y; + out_tri1.t[1].z = t * (outside_tex[0]->z - inside_tex[0]->z) + inside_tex[0]->z; + + out_tri1.p[2] = Math::Vec_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[1], t); + out_tri1.t[2].x = t * (outside_tex[1]->x - inside_tex[0]->x) + inside_tex[0]->x; + out_tri1.t[2].y = t * (outside_tex[1]->y - inside_tex[0]->y) + inside_tex[0]->y; + out_tri1.t[2].z = t * (outside_tex[1]->z - inside_tex[0]->z) + inside_tex[0]->z; + + return 1; // Return the newly formed single triangle + } + + if (nInsidePointCount == 2 && nOutsidePointCount == 1) + { + // Triangle should be clipped. As two points lie inside the plane, + // the clipped triangle becomes a "quad". Fortunately, we can + // represent a quad with two new triangles + + // Copy appearance info to new triangles + out_tri1.col = olc::GREEN;// in_tri.col; + out_tri2.col = olc::RED;// in_tri.col; + + // The first triangle consists of the two inside points and a new + // point determined by the location where one side of the triangle + // intersects with the plane + out_tri1.p[0] = *inside_points[0]; + out_tri1.t[0] = *inside_tex[0]; + + out_tri1.p[1] = *inside_points[1]; + out_tri1.t[1] = *inside_tex[1]; + + float t; + out_tri1.p[2] = Math::Vec_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[0], t); + out_tri1.t[2].x = t * (outside_tex[0]->x - inside_tex[0]->x) + inside_tex[0]->x; + out_tri1.t[2].y = t * (outside_tex[0]->y - inside_tex[0]->y) + inside_tex[0]->y; + out_tri1.t[2].z = t * (outside_tex[0]->z - inside_tex[0]->z) + inside_tex[0]->z; + + // The second triangle is composed of one of he inside points, a + // new point determined by the intersection of the other side of the + // triangle and the plane, and the newly created point above + out_tri2.p[1] = *inside_points[1]; + out_tri2.t[1] = *inside_tex[1]; + out_tri2.p[0] = out_tri1.p[2]; + out_tri2.t[0] = out_tri1.t[2]; + out_tri2.p[2] = Math::Vec_IntersectPlane(plane_p, plane_n, *inside_points[1], *outside_points[0], t); + out_tri2.t[2].x = t * (outside_tex[0]->x - inside_tex[1]->x) + inside_tex[1]->x; + out_tri2.t[2].y = t * (outside_tex[0]->y - inside_tex[1]->y) + inside_tex[1]->y; + out_tri2.t[2].z = t * (outside_tex[0]->z - inside_tex[1]->z) + inside_tex[1]->z; + return 2; // Return two newly formed triangles which form a quad + } + + return 0; + } + + void GFX3D::DrawTriangleFlat(olc::GFX3D::triangle &tri) + { + pge->FillTriangle(tri.p[0].x, tri.p[0].y, tri.p[1].x, tri.p[1].y, tri.p[2].x, tri.p[2].y, tri.col); + } + + void GFX3D::DrawTriangleWire(olc::GFX3D::triangle &tri, olc::Pixel col) + { + pge->DrawTriangle(tri.p[0].x, tri.p[0].y, tri.p[1].x, tri.p[1].y, tri.p[2].x, tri.p[2].y, col); + } + + void GFX3D::TexturedTriangle(int x1, int y1, float u1, float v1, float w1, + int x2, int y2, float u2, float v2, float w2, + int x3, int y3, float u3, float v3, float w3, olc::Sprite* spr) + + { + if (y2 < y1) + { + std::swap(y1, y2); + std::swap(x1, x2); + std::swap(u1, u2); + std::swap(v1, v2); + std::swap(w1, w2); + } + + if (y3 < y1) + { + std::swap(y1, y3); + std::swap(x1, x3); + std::swap(u1, u3); + std::swap(v1, v3); + std::swap(w1, w3); + } + + if (y3 < y2) + { + std::swap(y2, y3); + std::swap(x2, x3); + std::swap(u2, u3); + std::swap(v2, v3); + std::swap(w2, w3); + } + + int dy1 = y2 - y1; + int dx1 = x2 - x1; + float dv1 = v2 - v1; + float du1 = u2 - u1; + float dw1 = w2 - w1; + + int dy2 = y3 - y1; + int dx2 = x3 - x1; + float dv2 = v3 - v1; + float du2 = u3 - u1; + float dw2 = w3 - w1; + + float tex_u, tex_v, tex_w; + + float dax_step = 0, dbx_step = 0, + du1_step = 0, dv1_step = 0, + du2_step = 0, dv2_step = 0, + dw1_step = 0, dw2_step = 0; + + if (dy1) dax_step = dx1 / (float)abs(dy1); + if (dy2) dbx_step = dx2 / (float)abs(dy2); + + if (dy1) du1_step = du1 / (float)abs(dy1); + if (dy1) dv1_step = dv1 / (float)abs(dy1); + if (dy1) dw1_step = dw1 / (float)abs(dy1); + + if (dy2) du2_step = du2 / (float)abs(dy2); + if (dy2) dv2_step = dv2 / (float)abs(dy2); + if (dy2) dw2_step = dw2 / (float)abs(dy2); + + if (dy1) + { + for (int i = y1; i <= y2; i++) + { + int ax = x1 + (float)(i - y1) * dax_step; + int bx = x1 + (float)(i - y1) * dbx_step; + + float tex_su = u1 + (float)(i - y1) * du1_step; + float tex_sv = v1 + (float)(i - y1) * dv1_step; + float tex_sw = w1 + (float)(i - y1) * dw1_step; + + float tex_eu = u1 + (float)(i - y1) * du2_step; + float tex_ev = v1 + (float)(i - y1) * dv2_step; + float tex_ew = w1 + (float)(i - y1) * dw2_step; + + if (ax > bx) + { + std::swap(ax, bx); + std::swap(tex_su, tex_eu); + std::swap(tex_sv, tex_ev); + std::swap(tex_sw, tex_ew); + } + + tex_u = tex_su; + tex_v = tex_sv; + tex_w = tex_sw; + + float tstep = 1.0f / ((float)(bx - ax)); + float t = 0.0f; + + for (int j = ax; j < bx; j++) + { + tex_u = (1.0f - t) * tex_su + t * tex_eu; + tex_v = (1.0f - t) * tex_sv + t * tex_ev; + tex_w = (1.0f - t) * tex_sw + t * tex_ew; + if (tex_w > m_DepthBuffer[i*pge->ScreenWidth() + j]) + { + pge->Draw(j, i, spr->Sample(tex_u / tex_w, tex_v / tex_w)); + m_DepthBuffer[i*pge->ScreenWidth() + j] = tex_w; + } + t += tstep; + } + + } + } + + dy1 = y3 - y2; + dx1 = x3 - x2; + dv1 = v3 - v2; + du1 = u3 - u2; + dw1 = w3 - w2; + + if (dy1) dax_step = dx1 / (float)abs(dy1); + if (dy2) dbx_step = dx2 / (float)abs(dy2); + + du1_step = 0, dv1_step = 0; + if (dy1) du1_step = du1 / (float)abs(dy1); + if (dy1) dv1_step = dv1 / (float)abs(dy1); + if (dy1) dw1_step = dw1 / (float)abs(dy1); + + if (dy1) + { + for (int i = y2; i <= y3; i++) + { + int ax = x2 + (float)(i - y2) * dax_step; + int bx = x1 + (float)(i - y1) * dbx_step; + + float tex_su = u2 + (float)(i - y2) * du1_step; + float tex_sv = v2 + (float)(i - y2) * dv1_step; + float tex_sw = w2 + (float)(i - y2) * dw1_step; + + float tex_eu = u1 + (float)(i - y1) * du2_step; + float tex_ev = v1 + (float)(i - y1) * dv2_step; + float tex_ew = w1 + (float)(i - y1) * dw2_step; + + if (ax > bx) + { + std::swap(ax, bx); + std::swap(tex_su, tex_eu); + std::swap(tex_sv, tex_ev); + std::swap(tex_sw, tex_ew); + } + + tex_u = tex_su; + tex_v = tex_sv; + tex_w = tex_sw; + + float tstep = 1.0f / ((float)(bx - ax)); + float t = 0.0f; + + for (int j = ax; j < bx; j++) + { + tex_u = (1.0f - t) * tex_su + t * tex_eu; + tex_v = (1.0f - t) * tex_sv + t * tex_ev; + tex_w = (1.0f - t) * tex_sw + t * tex_ew; + + if (tex_w > m_DepthBuffer[i*pge->ScreenWidth() + j]) + { + pge->Draw(j, i, spr->Sample(tex_u / tex_w, tex_v / tex_w)); + m_DepthBuffer[i*pge->ScreenWidth() + j] = tex_w; + } + t += tstep; + } + } + } + } + + + void GFX3D::DrawTriangleTex(olc::GFX3D::triangle &tri, olc::Sprite* spr) + { + if (tri.p[1].y < tri.p[0].y) + { + std::swap(tri.p[0].y, tri.p[1].y); + std::swap(tri.p[0].x, tri.p[1].x); + std::swap(tri.t[0].x, tri.t[1].x); + std::swap(tri.t[0].y, tri.t[1].y); + std::swap(tri.t[0].z, tri.t[1].z); + } + + if (tri.p[2].y < tri.p[0].y) + { + std::swap(tri.p[0].y, tri.p[2].y); + std::swap(tri.p[0].x, tri.p[2].x); + std::swap(tri.t[0].x, tri.t[2].x); + std::swap(tri.t[0].y, tri.t[2].y); + std::swap(tri.t[0].z, tri.t[2].z); + } + + if (tri.p[2].y < tri.p[1].y) + { + std::swap(tri.p[1].y, tri.p[2].y); + std::swap(tri.p[1].x, tri.p[2].x); + std::swap(tri.t[1].x, tri.t[2].x); + std::swap(tri.t[1].y, tri.t[2].y); + std::swap(tri.t[1].z, tri.t[2].z); + } + + int dy1 = tri.p[1].y - tri.p[0].y; + int dx1 = tri.p[1].x - tri.p[0].x; + float dv1 = tri.t[1].y - tri.t[0].y; + float du1 = tri.t[1].x - tri.t[0].x; + float dz1 = tri.t[1].z - tri.t[0].z; + + int dy2 = tri.p[2].y - tri.p[0].y; + int dx2 = tri.p[2].x - tri.p[0].x; + float dv2 = tri.t[2].y - tri.t[0].y; + float du2 = tri.t[2].x - tri.t[0].x; + float dz2 = tri.t[2].z - tri.t[0].z; + + float tex_x, tex_y, tex_z; + + float du1_step = 0, dv1_step = 0, du2_step = 0, dv2_step = 0, dz1_step = 0, dz2_step = 0; + float dax_step = 0, dbx_step = 0; + + if (dy1) dax_step = dx1 / (float)abs(dy1); + if (dy2) dbx_step = dx2 / (float)abs(dy2); + + if (dy1) du1_step = du1 / (float)abs(dy1); + if (dy1) dv1_step = dv1 / (float)abs(dy1); + if (dy1) dz1_step = dz1 / (float)abs(dy1); + + if (dy2) du2_step = du2 / (float)abs(dy2); + if (dy2) dv2_step = dv2 / (float)abs(dy2); + if (dy2) dz2_step = dz2 / (float)abs(dy2); + + + + if (dy1) + { + for (int i = tri.p[0].y; i <= tri.p[1].y; i++) + { + int ax = tri.p[0].x + (i - tri.p[0].y) * dax_step; + int bx = tri.p[0].x + (i - tri.p[0].y) * dbx_step; + + // Start and end points in texture space + float tex_su = tri.t[0].x + (float)(i - tri.p[0].y) * du1_step; + float tex_sv = tri.t[0].y + (float)(i - tri.p[0].y) * dv1_step; + float tex_sz = tri.t[0].z + (float)(i - tri.p[0].y) * dz1_step; + + float tex_eu = tri.t[0].x + (float)(i - tri.p[0].y) * du2_step; + float tex_ev = tri.t[0].y + (float)(i - tri.p[0].y) * dv2_step; + float tex_ez = tri.t[0].z + (float)(i - tri.p[0].y) * dz2_step; + + if (ax > bx) + { + std::swap(ax, bx); + std::swap(tex_su, tex_eu); + std::swap(tex_sv, tex_ev); + std::swap(tex_sz, tex_ez); + } + + tex_x = tex_su; + tex_y = tex_sv; + tex_z = tex_sz; + + + float tstep = 1.0f / ((float)(bx - ax)); + float t = 0; + + for (int j = ax; j < bx; j++) + { + tex_x = (1.0f - t) * tex_su + t * tex_eu; + tex_y = (1.0f - t) * tex_sv + t * tex_ev; + tex_z = (1.0f - t) * tex_sz + t * tex_ez; + + if (tex_z > m_DepthBuffer[i*pge->ScreenWidth() + j]) + { + pge->Draw(j, i, spr->Sample(tex_x / tex_z, tex_y / tex_z)); + m_DepthBuffer[i*pge->ScreenWidth() + j] = tex_z; + } + t += tstep; + } + + + } + } + + dy1 = tri.p[2].y - tri.p[1].y; + dx1 = tri.p[2].x - tri.p[1].x; + dv1 = tri.t[2].y - tri.t[1].y; + du1 = tri.t[2].x - tri.t[1].x; + dz1 = tri.t[2].z - tri.t[1].z; + + if (dy1) dax_step = dx1 / (float)abs(dy1); + if (dy2) dbx_step = dx2 / (float)abs(dy2); + + + du1_step = 0, dv1_step = 0;// , dz1_step = 0;// , du2_step = 0, dv2_step = 0; + if (dy1) du1_step = du1 / (float)abs(dy1); + if (dy1) dv1_step = dv1 / (float)abs(dy1); + if (dy1) dz1_step = dz1 / (float)abs(dy1); + + if (dy1) + { + for (int i = tri.p[1].y; i <= tri.p[2].y; i++) + { + int ax = tri.p[1].x + (i - tri.p[1].y) * dax_step; + int bx = tri.p[0].x + (i - tri.p[0].y) * dbx_step; + + // Start and end points in texture space + float tex_su = tri.t[1].x + (float)(i - tri.p[1].y) * du1_step; + float tex_sv = tri.t[1].y + (float)(i - tri.p[1].y) * dv1_step; + float tex_sz = tri.t[1].z + (float)(i - tri.p[1].y) * dz1_step; + + float tex_eu = tri.t[0].x + (float)(i - tri.p[0].y) * du2_step; + float tex_ev = tri.t[0].y + (float)(i - tri.p[0].y) * dv2_step; + float tex_ez = tri.t[0].z + (float)(i - tri.p[0].y) * dz2_step; + + if (ax > bx) + { + std::swap(ax, bx); + std::swap(tex_su, tex_eu); + std::swap(tex_sv, tex_ev); + std::swap(tex_sz, tex_ez); + } + + tex_x = tex_su; + tex_y = tex_sv; + tex_z = tex_sz; + + + float tstep = 1.0f / ((float)(bx - ax)); + float t = 0; + + for (int j = ax; j < bx; j++) + { + tex_x = (1.0f - t) * tex_su + t * tex_eu; + tex_y = (1.0f - t) * tex_sv + t * tex_ev; + tex_z = (1.0f - t) * tex_sz + t * tex_ez; + + if (tex_z > m_DepthBuffer[i*pge->ScreenWidth() + j]) + { + pge->Draw(j, i, spr->Sample(tex_x / tex_z, tex_y / tex_z)); + m_DepthBuffer[i*pge->ScreenWidth() + j] = tex_z; + } + + t += tstep; + } + } + } + + } + + float* GFX3D::m_DepthBuffer = nullptr; + + void GFX3D::ConfigureDisplay() + { + m_DepthBuffer = new float[pge->ScreenWidth() * pge->ScreenHeight()]{ 0 }; + } + + + void GFX3D::ClearDepth() + { + memset(m_DepthBuffer, 0, pge->ScreenWidth() * pge->ScreenHeight() * sizeof(float)); + } + + + + + GFX3D::PipeLine::PipeLine() + { + + } + + void GFX3D::PipeLine::SetProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar, float fLeft, float fTop, float fWidth, float fHeight) + { + matProj = GFX3D::Math::Mat_MakeProjection(fFovDegrees, fAspectRatio, fNear, fFar); + fViewX = fLeft; + fViewY = fTop; + fViewW = fWidth; + fViewH = fHeight; + } + + void GFX3D::PipeLine::SetCamera(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &lookat, olc::GFX3D::vec3d &up) + { + matView = GFX3D::Math::Mat_PointAt(pos, lookat, up); + matView = GFX3D::Math::Mat_QuickInverse(matView); + } + + void GFX3D::PipeLine::SetTransform(olc::GFX3D::mat4x4 &transform) + { + matWorld = transform; + } + + void GFX3D::PipeLine::SetTexture(olc::Sprite *texture) + { + sprTexture = texture; + } + + void GFX3D::PipeLine::SetLightSource(olc::GFX3D::vec3d &pos, olc::GFX3D::vec3d &dir, olc::Pixel &col) + { + + } + + uint32_t GFX3D::PipeLine::Render(std::vector &triangles, uint32_t flags) + { + // Calculate Transformation Matrix + mat4x4 matWorldView = Math::Mat_MultiplyMatrix(matWorld, matView); + //matWorldViewProj = Math::Mat_MultiplyMatrix(matWorldView, matProj); + + // Store triangles for rastering later + std::vector vecTrianglesToRaster; + + int nTriangleDrawnCount = 0; + + // Process Triangles + for (auto &tri : triangles) + { + GFX3D::triangle triTransformed; + + // Just copy through texture coordinates + triTransformed.t[0] = { tri.t[0].x, tri.t[0].y, tri.t[0].z }; + triTransformed.t[1] = { tri.t[1].x, tri.t[1].y, tri.t[1].z }; + triTransformed.t[2] = { tri.t[2].x, tri.t[2].y, tri.t[2].z }; // Think! + + // Transform Triangle from object into projected space + triTransformed.p[0] = GFX3D::Math::Mat_MultiplyVector(matWorldView, tri.p[0]); + triTransformed.p[1] = GFX3D::Math::Mat_MultiplyVector(matWorldView, tri.p[1]); + triTransformed.p[2] = GFX3D::Math::Mat_MultiplyVector(matWorldView, tri.p[2]); + + // Calculate Triangle Normal in WorldView Space + GFX3D::vec3d normal, line1, line2; + line1 = GFX3D::Math::Vec_Sub(triTransformed.p[1], triTransformed.p[0]); + line2 = GFX3D::Math::Vec_Sub(triTransformed.p[2], triTransformed.p[0]); + normal = GFX3D::Math::Vec_CrossProduct(line1, line2); + normal = GFX3D::Math::Vec_Normalise(normal); + + // Cull triangles that face away from viewer + if (flags & RENDER_CULL_CW && GFX3D::Math::Vec_DotProduct(normal, triTransformed.p[0]) > 0.0f) continue; + if (flags & RENDER_CULL_CCW && GFX3D::Math::Vec_DotProduct(normal, triTransformed.p[0]) < 0.0f) continue; + + // If Lighting, calculate shading + triTransformed.col = olc::WHITE; + + // Clip triangle against near plane + int nClippedTriangles = 0; + triangle clipped[2]; + nClippedTriangles = GFX3D::Math::Triangle_ClipAgainstPlane({ 0.0f, 0.0f, 0.1f }, { 0.0f, 0.0f, 1.0f }, triTransformed, clipped[0], clipped[1]); + + // This may yield two new triangles + for (int n = 0; n < nClippedTriangles; n++) + { + triangle triProjected = clipped[n]; + + // Project new triangle + triProjected.p[0] = GFX3D::Math::Mat_MultiplyVector(matProj, clipped[n].p[0]); + triProjected.p[1] = GFX3D::Math::Mat_MultiplyVector(matProj, clipped[n].p[1]); + triProjected.p[2] = GFX3D::Math::Mat_MultiplyVector(matProj, clipped[n].p[2]); + + // Apply Projection to Verts + triProjected.p[0].x = triProjected.p[0].x / triProjected.p[0].w; + triProjected.p[1].x = triProjected.p[1].x / triProjected.p[1].w; + triProjected.p[2].x = triProjected.p[2].x / triProjected.p[2].w; + + triProjected.p[0].y = triProjected.p[0].y / triProjected.p[0].w; + triProjected.p[1].y = triProjected.p[1].y / triProjected.p[1].w; + triProjected.p[2].y = triProjected.p[2].y / triProjected.p[2].w; + + triProjected.p[0].z = triProjected.p[0].z / triProjected.p[0].w; + triProjected.p[1].z = triProjected.p[1].z / triProjected.p[1].w; + triProjected.p[2].z = triProjected.p[2].z / triProjected.p[2].w; + + // Apply Projection to Tex coords + triProjected.t[0].x = triProjected.t[0].x / triProjected.p[0].w; + triProjected.t[1].x = triProjected.t[1].x / triProjected.p[1].w; + triProjected.t[2].x = triProjected.t[2].x / triProjected.p[2].w; + + triProjected.t[0].y = triProjected.t[0].y / triProjected.p[0].w; + triProjected.t[1].y = triProjected.t[1].y / triProjected.p[1].w; + triProjected.t[2].y = triProjected.t[2].y / triProjected.p[2].w; + + triProjected.t[0].z = 1.0f / triProjected.p[0].w; + triProjected.t[1].z = 1.0f / triProjected.p[1].w; + triProjected.t[2].z = 1.0f / triProjected.p[2].w; + + // Clip against viewport in screen space + // Clip triangles against all four screen edges, this could yield + // a bunch of triangles, so create a queue that we traverse to + // ensure we only test new triangles generated against planes + triangle sclipped[2]; + std::list listTriangles; + + + // Add initial triangle + listTriangles.push_back(triProjected); + int nNewTriangles = 1; + + for (int p = 0; p < 4; p++) + { + int nTrisToAdd = 0; + while (nNewTriangles > 0) + { + // Take triangle from front of queue + triangle test = listTriangles.front(); + listTriangles.pop_front(); + nNewTriangles--; + + // Clip it against a plane. We only need to test each + // subsequent plane, against subsequent new triangles + // as all triangles after a plane clip are guaranteed + // to lie on the inside of the plane. I like how this + // comment is almost completely and utterly justified + switch (p) + { + case 0: nTrisToAdd = GFX3D::Math::Triangle_ClipAgainstPlane({ 0.0f, -1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, test, sclipped[0], sclipped[1]); break; + case 1: nTrisToAdd = GFX3D::Math::Triangle_ClipAgainstPlane({ 0.0f, +1.0f, 0.0f }, { 0.0f, -1.0f, 0.0f }, test, sclipped[0], sclipped[1]); break; + case 2: nTrisToAdd = GFX3D::Math::Triangle_ClipAgainstPlane({ -1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f }, test, sclipped[0], sclipped[1]); break; + case 3: nTrisToAdd = GFX3D::Math::Triangle_ClipAgainstPlane({ +1.0f, 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f }, test, sclipped[0], sclipped[1]); break; + } + + + // Clipping may yield a variable number of triangles, so + // add these new ones to the back of the queue for subsequent + // clipping against next planes + for (int w = 0; w < nTrisToAdd; w++) + listTriangles.push_back(sclipped[w]); + } + nNewTriangles = listTriangles.size(); + } + + for (auto &triRaster : listTriangles) + { + // Scale to viewport + /*triRaster.p[0].x *= -1.0f; + triRaster.p[1].x *= -1.0f; + triRaster.p[2].x *= -1.0f; + triRaster.p[0].y *= -1.0f; + triRaster.p[1].y *= -1.0f; + triRaster.p[2].y *= -1.0f;*/ + vec3d vOffsetView = { 1,1,0 }; + triRaster.p[0] = Math::Vec_Add(triRaster.p[0], vOffsetView); + triRaster.p[1] = Math::Vec_Add(triRaster.p[1], vOffsetView); + triRaster.p[2] = Math::Vec_Add(triRaster.p[2], vOffsetView); + triRaster.p[0].x *= 0.5f * fViewW; + triRaster.p[0].y *= 0.5f * fViewH; + triRaster.p[1].x *= 0.5f * fViewW; + triRaster.p[1].y *= 0.5f * fViewH; + triRaster.p[2].x *= 0.5f * fViewW; + triRaster.p[2].y *= 0.5f * fViewH; + vOffsetView = { fViewX,fViewY,0 }; + triRaster.p[0] = Math::Vec_Add(triRaster.p[0], vOffsetView); + triRaster.p[1] = Math::Vec_Add(triRaster.p[1], vOffsetView); + triRaster.p[2] = Math::Vec_Add(triRaster.p[2], vOffsetView); + + // For now, just draw triangle + + if (flags & RENDER_TEXTURED) + { + TexturedTriangle( + triRaster.p[0].x, triRaster.p[0].y, triRaster.t[0].x, triRaster.t[0].y, triRaster.t[0].z, + triRaster.p[1].x, triRaster.p[1].y, triRaster.t[1].x, triRaster.t[1].y, triRaster.t[1].z, + triRaster.p[2].x, triRaster.p[2].y, triRaster.t[2].x, triRaster.t[2].y, triRaster.t[2].z, + sprTexture); + } + + if (flags & RENDER_WIRE) + { + DrawTriangleWire(triRaster, olc::RED); + } + + if (flags & RENDER_FLAT) + { + DrawTriangleFlat(triRaster); + } + + nTriangleDrawnCount++; + } + } + } + + return nTriangleDrawnCount; + } +} + +#endif \ No newline at end of file diff --git a/CarCrimeCity/Part1/olcPixelGameEngine.h b/CarCrimeCity/Part1/olcPixelGameEngine.h new file mode 100644 index 0000000..37c3ae8 --- /dev/null +++ b/CarCrimeCity/Part1/olcPixelGameEngine.h @@ -0,0 +1,2067 @@ +/* + olcPixelGameEngine.h + + +-------------------------------------------------------------+ + | OneLoneCoder Pixel Game Engine v1.12 | + | "Like the command prompt console one, but not..." - javidx9 | + +-------------------------------------------------------------+ + + What is this? + ~~~~~~~~~~~~~ + The olcConsoleGameEngine has been a surprsing and wonderful + success for me, and I'm delighted how people have reacted so + positively towards it, so thanks for that. + + However, there are limitations that I simply cannot avoid. + Firstly, I need to maintain several different versions of + it to accommodate users on Windows7, 8, 10, Linux, Mac, + Visual Studio & Code::Blocks. Secondly, this year I've been + pushing the console to the limits of its graphical capabilities + and the effect is becoming underwhelming. The engine itself + is not slow at all, but the process that Windows uses to + draw the command prompt to the screen is, and worse still, + it's dynamic based upon the variation of character colours + and glyphs. Sadly I have no control over this, and recent + videos that are extremely graphical (for a command prompt :P ) + have been dipping to unacceptable framerates. As the channel + has been popular with aspiring game developers, I'm concerned + that the visual appeal of the command prompt is perhaps + limited to us oldies, and I dont want to alienate younger + learners. Finally, I'd like to demonstrate many more + algorithms and image processing that exist in the graphical + domain, for which the console is insufficient. + + For this reason, I have created olcPixelGameEngine! The look + and feel to the programmer is almost identical, so all of my + existing code from the videos is easily portable, and the + programmer uses this file in exactly the same way. But I've + decided that rather than just build a command prompt emulator, + that I would at least harness some modern(ish) portable + technologies. + + As a result, the olcPixelGameEngine supports 32-bit colour, is + written in a cross-platform style, uses modern(ish) C++ + conventions and most importantly, renders much much faster. I + will use this version when my applications are predominantly + graphics based, but use the console version when they are + predominantly text based - Don't worry, loads more command + prompt silliness to come yet, but evolution is important!! + + License (OLC-3) + ~~~~~~~~~~~~~~~ + + Copyright 2018 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 + Patreon: https://www.patreon.com/javidx9 + + Relevant Videos + ~~~~~~~~~~~~~~~ + https://youtu.be/kRH6oJLFYxY Introducing olcPixelGameEngine + + Compiling in Linux + ~~~~~~~~~~~~~~~~~~ + You will need a modern C++ compiler, so update yours! + To compile use the command: + + g++ -o YourProgName YourSource.cpp -lX11 -lGL -lpthread -lpng + + On some Linux configurations, the frame rate is locked to the refresh + rate of the monitor. This engine tries to unlock it but may not be + able to, in which case try launching your program like this: + + vblank_mode=0 ./YourProgName + + + Compiling in Code::Blocks on Windows + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Well I wont judge you, but make sure your Code::Blocks installation + is really up to date - you may even consider updating your C++ toolchain + to use MinGW32-W64, so google this. You will also need to enable C++14 + in your build options, and add to your linker the following libraries: + user32 gdi32 opengl32 gdiplus + + Thanks + ~~~~~~ + I'd like to extend thanks to Eremiell, slavka, gurkanctn, Phantim, + JackOJC, KrossX, Huhlig, Dragoneye, Appa, JustinRichardsMusic, SliceNDice + Ralakus, Gorbit99, raoul & MagetzUb for advice, ideas and testing, and I'd like + to extend my appreciation to the 23K YouTube followers and 1.5K Discord server + members who give me the motivation to keep going with all this :D + + Special thanks to those who bring gifts! + GnarGnarHead.......Domina + Gorbit99...........Bastion + + Special thanks to my Patreons too - I wont name you on here, but I've + certainly enjoyed my tea and flapjacks :D + + Author + ~~~~~~ + David Barr, aka javidx9, ©OneLoneCoder 2018, 2019 +*/ + +////////////////////////////////////////////////////////////////////////////////////////// + +/* Example Usage (main.cpp) + #define OLC_PGE_APPLICATION + #include "olcPixelGameEngine.h" + // Override base class with your custom functionality + class Example : public olc::PixelGameEngine + { + public: + Example() + { + sAppName = "Example"; + } + public: + bool OnUserCreate() override + { + // Called once at the start, so create things here + return true; + } + bool OnUserUpdate(float fElapsedTime) override + { + // called once per frame, draws random coloured pixels + for (int x = 0; x < ScreenWidth(); x++) + for (int y = 0; y < ScreenHeight(); y++) + Draw(x, y, olc::Pixel(rand() % 255, rand() % 255, rand()% 255)); + return true; + } + }; + int main() + { + Example demo; + if (demo.Construct(256, 240, 4, 4)) + demo.Start(); + return 0; + } +*/ + +#ifndef OLC_PGE_DEF +#define OLC_PGE_DEF + +#ifdef _WIN32 + // Link to libraries +#ifndef __MINGW32__ + #pragma comment(lib, "user32.lib") // Visual Studio Only + #pragma comment(lib, "gdi32.lib") // For other Windows Compilers please add + #pragma comment(lib, "opengl32.lib") // these libs to your linker input + #pragma comment(lib, "gdiplus.lib") +#else + // In Code::Blocks, Select C++14 in your build options, and add the + // following libs to your linker: user32 gdi32 opengl32 gdiplus +#endif + // Include WinAPI + #include + #include + + // OpenGL Extension + #include + typedef BOOL(WINAPI wglSwapInterval_t) (int interval); + static wglSwapInterval_t *wglSwapInterval; +#else + #include + #include + #include + #include + #include + typedef int(glSwapInterval_t) (Display *dpy, GLXDrawable drawable, int interval); + static glSwapInterval_t *glSwapIntervalEXT; +#endif + + +// Standard includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef min +#undef max + +namespace olc // All OneLoneCoder stuff will now exist in the "olc" namespace +{ + struct Pixel + { + union + { + uint32_t n = 0xFF000000; + struct + { + uint8_t r; uint8_t g; uint8_t b; uint8_t a; + }; + }; + + Pixel(); + Pixel(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha = 255); + Pixel(uint32_t p); + enum Mode { NORMAL, MASK, ALPHA, CUSTOM }; + }; + + // Some constants for symbolic naming of Pixels + static const Pixel + WHITE(255, 255, 255), + GREY(192, 192, 192), DARK_GREY(128, 128, 128), VERY_DARK_GREY(64, 64, 64), + RED(255, 0, 0), DARK_RED(128, 0, 0), VERY_DARK_RED(64, 0, 0), + YELLOW(255, 255, 0), DARK_YELLOW(128, 128, 0), VERY_DARK_YELLOW(64, 64, 0), + GREEN(0, 255, 0), DARK_GREEN(0, 128, 0), VERY_DARK_GREEN(0, 64, 0), + CYAN(0, 255, 255), DARK_CYAN(0, 128, 128), VERY_DARK_CYAN(0, 64, 64), + BLUE(0, 0, 255), DARK_BLUE(0, 0, 128), VERY_DARK_BLUE(0, 0, 64), + MAGENTA(255, 0, 255), DARK_MAGENTA(128, 0, 128), VERY_DARK_MAGENTA(64, 0, 64), + BLACK(0, 0, 0), + BLANK(0, 0, 0, 0); + + enum rcode + { + FAIL = 0, + OK = 1, + NO_FILE = -1, + }; + + //============================================================= + + struct HWButton + { + bool bPressed = false; // Set once during the frame the event occurs + bool bReleased = false; // Set once during the frame the event occurs + bool bHeld = false; // Set tru for all frames between pressed and released events + }; + + //============================================================= + + class ResourcePack + { + public: + ResourcePack(); + ~ResourcePack(); + struct sEntry : public std::streambuf { + uint32_t nID, nFileOffset, nFileSize; uint8_t* data; void _config() { this->setg((char*)data, (char*)data, (char*)(data + nFileSize)); } + }; + + public: + olc::rcode AddToPack(std::string sFile); + + public: + olc::rcode SavePack(std::string sFile); + olc::rcode LoadPack(std::string sFile); + olc::rcode ClearPack(); + + public: + olc::ResourcePack::sEntry GetStreamBuffer(std::string sFile); + + private: + + std::map mapFiles; + }; + + //============================================================= + + // A bitmap-like structure that stores a 2D array of Pixels + class Sprite + { + public: + Sprite(); + Sprite(std::string sImageFile); + Sprite(std::string sImageFile, olc::ResourcePack *pack); + Sprite(int32_t w, int32_t h); + ~Sprite(); + + public: + olc::rcode LoadFromFile(std::string sImageFile, olc::ResourcePack *pack = nullptr); + olc::rcode LoadFromPGESprFile(std::string sImageFile, olc::ResourcePack *pack = nullptr); + olc::rcode SaveToPGESprFile(std::string sImageFile); + + public: + int32_t width = 0; + int32_t height = 0; + enum Mode { NORMAL, PERIODIC }; + + public: + void SetSampleMode(olc::Sprite::Mode mode = olc::Sprite::Mode::NORMAL); + Pixel GetPixel(int32_t x, int32_t y); + void SetPixel(int32_t x, int32_t y, Pixel p); + Pixel Sample(float x, float y); + Pixel* GetData(); + + private: + Pixel *pColData = nullptr; + Mode modeSample = Mode::NORMAL; + +#ifdef OLC_DBG_OVERDRAW + public: + static int nOverdrawCount; +#endif + + }; + + //============================================================= + + enum Key + { + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, + K0, K1, K2, K3, K4, K5, K6, K7, K8, K9, + F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, + UP, DOWN, LEFT, RIGHT, + SPACE, TAB, SHIFT, CTRL, INS, DEL, HOME, END, PGUP, PGDN, + BACK, ESCAPE, RETURN, ENTER, PAUSE, SCROLL, + NP0, NP1, NP2, NP3, NP4, NP5, NP6, NP7, NP8, NP9, + NP_MUL, NP_DIV, NP_ADD, NP_SUB, NP_DECIMAL, + }; + + + //============================================================= + + class PixelGameEngine + { + public: + PixelGameEngine(); + + public: + olc::rcode Construct(uint32_t screen_w, uint32_t screen_h, uint32_t pixel_w, uint32_t pixel_h); + olc::rcode Start(); + + public: // Override Interfaces + // Called once on application startup, use to load your resources + virtual bool OnUserCreate(); + // Called every frame, and provides you with a time per frame value + virtual bool OnUserUpdate(float fElapsedTime); + // Called once on application termination, so you can be a clean coder + virtual bool OnUserDestroy(); + + public: // Hardware Interfaces + // Returns true if window is currently in focus + bool IsFocused(); + // Get the state of a specific keyboard button + HWButton GetKey(Key k); + // Get the state of a specific mouse button + HWButton GetMouse(uint32_t b); + // Get Mouse X coordinate in "pixel" space + int32_t GetMouseX(); + // Get Mouse Y coordinate in "pixel" space + int32_t GetMouseY(); + + public: // Utility + // Returns the width of the screen in "pixels" + int32_t ScreenWidth(); + // Returns the height of the screen in "pixels" + int32_t ScreenHeight(); + // Returns the width of the currently selected drawing target in "pixels" + int32_t GetDrawTargetWidth(); + // Returns the height of the currently selected drawing target in "pixels" + int32_t GetDrawTargetHeight(); + // Returns the currently active draw target + Sprite* GetDrawTarget(); + + public: // Draw Routines + // Specify which Sprite should be the target of drawing functions, use nullptr + // to specify the primary screen + void SetDrawTarget(Sprite *target); + // Change the pixel mode for different optimisations + // olc::Pixel::NORMAL = No transparency + // olc::Pixel::MASK = Transparent if alpha is < 255 + // olc::Pixel::ALPHA = Full transparency + void SetPixelMode(Pixel::Mode m); + Pixel::Mode GetPixelMode(); + // Use a custom blend function + void SetPixelMode(std::function pixelMode); + // Change the blend factor form between 0.0f to 1.0f; + void SetPixelBlend(float fBlend); + // Offset texels by sub-pixel amount (advanced, do not use) + void SetSubPixelOffset(float ox, float oy); + + // Draws a single Pixel + virtual void Draw(int32_t x, int32_t y, Pixel p = olc::WHITE); + // Draws a line from (x1,y1) to (x2,y2) + void DrawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, Pixel p = olc::WHITE); + // Draws a circle located at (x,y) with radius + void DrawCircle(int32_t x, int32_t y, int32_t radius, Pixel p = olc::WHITE); + // Fills a circle located at (x,y) with radius + void FillCircle(int32_t x, int32_t y, int32_t radius, Pixel p = olc::WHITE); + // Draws a rectangle at (x,y) to (x+w,y+h) + void DrawRect(int32_t x, int32_t y, int32_t w, int32_t h, Pixel p = olc::WHITE); + // Fills a rectangle at (x,y) to (x+w,y+h) + void FillRect(int32_t x, int32_t y, int32_t w, int32_t h, Pixel p = olc::WHITE); + // Draws a triangle between points (x1,y1), (x2,y2) and (x3,y3) + void DrawTriangle(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, Pixel p = olc::WHITE); + // Flat fills a triangle between points (x1,y1), (x2,y2) and (x3,y3) + void FillTriangle(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, Pixel p = olc::WHITE); + // Draws an entire sprite at location (x,y) + void DrawSprite(int32_t x, int32_t y, Sprite *sprite, uint32_t scale = 1); + // Draws an area of a sprite at location (x,y), where the + // selected area is (ox,oy) to (ox+w,oy+h) + void DrawPartialSprite(int32_t x, int32_t y, Sprite *sprite, int32_t ox, int32_t oy, int32_t w, int32_t h, uint32_t scale = 1); + // Draws a single line of text + void DrawString(int32_t x, int32_t y, std::string sText, Pixel col = olc::WHITE, uint32_t scale = 1); + // Clears entire draw target to Pixel + void Clear(Pixel p); + + public: // Branding + std::string sAppName; + + private: // Inner mysterious workings + Sprite *pDefaultDrawTarget = nullptr; + Sprite *pDrawTarget = nullptr; + Pixel::Mode nPixelMode = Pixel::NORMAL; + float fBlendFactor = 1.0f; + uint32_t nScreenWidth = 256; + uint32_t nScreenHeight = 240; + uint32_t nPixelWidth = 4; + uint32_t nPixelHeight = 4; + int32_t nMousePosX = 0; + int32_t nMousePosY = 0; + float fPixelX = 1.0f; + float fPixelY = 1.0f; + float fSubPixelOffsetX = 0.0f; + float fSubPixelOffsetY = 0.0f; + bool bHasInputFocus = false; + bool bHasMouseFocus = false; + float fFrameTimer = 1.0f; + int nFrameCount = 0; + Sprite *fontSprite = nullptr; + std::function funcPixelMode; + + static std::map mapKeys; + bool pKeyNewState[256]{ 0 }; + bool pKeyOldState[256]{ 0 }; + HWButton pKeyboardState[256]; + + bool pMouseNewState[5]{ 0 }; + bool pMouseOldState[5]{ 0 }; + HWButton pMouseState[5]; + +#ifdef _WIN32 + HDC glDeviceContext = nullptr; + HGLRC glRenderContext = nullptr; +#else + GLXContext glDeviceContext = nullptr; + GLXContext glRenderContext = nullptr; +#endif + GLuint glBuffer; + + void EngineThread(); + + // If anything sets this flag to false, the engine + // "should" shut down gracefully + static std::atomic bAtomActive; + + // Common initialisation functions + void olc_UpdateMouse(int32_t x, int32_t y); + bool olc_OpenGLCreate(); + void olc_ConstructFontSheet(); + +#ifdef _WIN32 + // Windows specific window handling + HWND olc_hWnd = nullptr; + HWND olc_WindowCreate(); + std::wstring wsAppName; + static LRESULT CALLBACK olc_WindowEvent(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); +#else + // Non-Windows specific window handling + Display* olc_Display = nullptr; + Window olc_WindowRoot; + Window olc_Window; + XVisualInfo* olc_VisualInfo; + Colormap olc_ColourMap; + XSetWindowAttributes olc_SetWindowAttribs; + Display* olc_WindowCreate(); +#endif + + }; + + + class PGEX + { + friend class olc::PixelGameEngine; + protected: + static PixelGameEngine* pge; + }; + + //============================================================= +} + +#endif // OLC_PGE_DEF + + + + +/* + Object Oriented Mode + ~~~~~~~~~~~~~~~~~~~~ + + If the olcPixelGameEngine.h is called from several sources it can cause + multiple definitions of objects. To prevent this, ONLY ONE of the pathways + to including this file must have OLC_PGE_APPLICATION defined before it. This prevents + the definitions being duplicated. + + Consider the following project structure: + + Class1.h - Includes olcPixelGameEngine.h, overrides olc::PixelGameEngine + Class1.cpp - #define OLC_PGE_APPLICATION #include "Class1.h" + Class2.h - Includes Class1.h, which includes olcPixelGameEngine.h + Class2.cpp - #define OLC_PGE_APPLICATION #include "Class2.h" + main.cpp - Includes Class1.h and Class2.h + + If all of this is a bit too confusing, you can split this file in two! + Everything below this comment block can go into olcPixelGameEngineOOP.cpp + and everything above it can go into olcPixelGameEngineOOP.h + +*/ + +#ifdef OLC_PGE_APPLICATION +#undef OLC_PGE_APPLICATION + +namespace olc +{ + Pixel::Pixel() + { + r = 0; g = 0; b = 0; a = 255; + } + + Pixel::Pixel(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) + { + r = red; g = green; b = blue; a = alpha; + } + + Pixel::Pixel(uint32_t p) + { + n = p; + } + + //========================================================== + + std::wstring ConvertS2W(std::string s) + { +#ifdef _WIN32 + int count = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, NULL, 0); + wchar_t* buffer = new wchar_t[count]; + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, buffer, count); + std::wstring w(buffer); + delete[] buffer; + return w; +#endif +//#ifdef __MINGW32__ +// wchar_t *buffer = new wchar_t[sImageFile.length() + 1]; +// mbstowcs(buffer, sImageFile.c_str(), sImageFile.length()); +// buffer[sImageFile.length()] = L'\0'; +// wsImageFile = buffer; +// delete[] buffer; +//#else + } + + Sprite::Sprite() + { + pColData = nullptr; + width = 0; + height = 0; + } + + Sprite::Sprite(std::string sImageFile) + { + LoadFromFile(sImageFile); + } + + Sprite::Sprite(std::string sImageFile, olc::ResourcePack *pack) + { + LoadFromPGESprFile(sImageFile, pack); + } + + Sprite::Sprite(int32_t w, int32_t h) + { + if(pColData) delete[] pColData; + width = w; height = h; + pColData = new Pixel[width * height]; + for (int32_t i = 0; i < width*height; i++) + pColData[i] = Pixel(); + } + + Sprite::~Sprite() + { + if (pColData) delete pColData; + } + + olc::rcode Sprite::LoadFromPGESprFile(std::string sImageFile, olc::ResourcePack *pack) + { + if (pColData) delete[] pColData; + + auto ReadData = [&](std::istream &is) + { + is.read((char*)&width, sizeof(int32_t)); + is.read((char*)&height, sizeof(int32_t)); + pColData = new Pixel[width * height]; + is.read((char*)pColData, width * height * sizeof(uint32_t)); + }; + + // These are essentially Memory Surfaces represented by olc::Sprite + // which load very fast, but are completely uncompressed + if (pack == nullptr) + { + std::ifstream ifs; + ifs.open(sImageFile, std::ifstream::binary); + if (ifs.is_open()) + { + ReadData(ifs); + return olc::OK; + } + else + return olc::FAIL; + } + else + { + auto streamBuffer = pack->GetStreamBuffer(sImageFile); + std::istream is(&streamBuffer); + ReadData(is); + } + + + return olc::FAIL; + } + + olc::rcode Sprite::SaveToPGESprFile(std::string sImageFile) + { + if (pColData == nullptr) return olc::FAIL; + + std::ofstream ofs; + ofs.open(sImageFile, std::ifstream::binary); + if (ofs.is_open()) + { + ofs.write((char*)&width, sizeof(int32_t)); + ofs.write((char*)&height, sizeof(int32_t)); + ofs.write((char*)pColData, width*height*sizeof(uint32_t)); + ofs.close(); + return olc::OK; + } + + return olc::FAIL; + } + + olc::rcode Sprite::LoadFromFile(std::string sImageFile, olc::ResourcePack *pack) + { +#ifdef _WIN32 + // Use GDI+ + std::wstring wsImageFile; +#ifdef __MINGW32__ + wchar_t *buffer = new wchar_t[sImageFile.length() + 1]; + mbstowcs(buffer, sImageFile.c_str(), sImageFile.length()); + buffer[sImageFile.length()] = L'\0'; + wsImageFile = buffer; + delete [] buffer; +#else + wsImageFile = ConvertS2W(sImageFile); +#endif + Gdiplus::Bitmap *bmp = Gdiplus::Bitmap::FromFile(wsImageFile.c_str()); + if (bmp == nullptr) + return olc::NO_FILE; + + width = bmp->GetWidth(); + height = bmp->GetHeight(); + pColData = new Pixel[width * height]; + + for(int x=0; xGetPixel(x, y, &c); + SetPixel(x, y, Pixel(c.GetRed(), c.GetGreen(), c.GetBlue(), c.GetAlpha())); + } + delete bmp; + return olc::OK; +#else + //////////////////////////////////////////////////////////////////////////// + // Use libpng, Thanks to Guillaume Cottenceau + // https://gist.github.com/niw/5963798 + png_structp png; + png_infop info; + + FILE *f = fopen(sImageFile.c_str(), "rb"); + if (!f) return olc::NO_FILE; + + png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) goto fail_load; + + info = png_create_info_struct(png); + if (!info) goto fail_load; + + if (setjmp(png_jmpbuf(png))) goto fail_load; + + png_init_io(png, f); + png_read_info(png, info); + + png_byte color_type; + png_byte bit_depth; + png_bytep *row_pointers; + width = png_get_image_width(png, info); + height = png_get_image_height(png, info); + color_type = png_get_color_type(png, info); + bit_depth = png_get_bit_depth(png, info); + +#ifdef _DEBUG + std::cout << "Loading PNG: " << sImageFile << "\n"; + std::cout << "W:" << width << " H:" << height << " D:" << (int)bit_depth << "\n"; +#endif + + if (bit_depth == 16) png_set_strip_16(png); + if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png); + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png); + if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png); + if (color_type == PNG_COLOR_TYPE_RGB || + color_type == PNG_COLOR_TYPE_GRAY || + color_type == PNG_COLOR_TYPE_PALETTE) + png_set_filler(png, 0xFF, PNG_FILLER_AFTER); + if (color_type == PNG_COLOR_TYPE_GRAY || + color_type == PNG_COLOR_TYPE_GRAY_ALPHA) + png_set_gray_to_rgb(png); + + png_read_update_info(png, info); + row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * height); + for (int y = 0; y < height; y++) { + row_pointers[y] = (png_byte*)malloc(png_get_rowbytes(png, info)); + } + png_read_image(png, row_pointers); + //////////////////////////////////////////////////////////////////////////// + + // Create sprite array + pColData = new Pixel[width * height]; + + // Iterate through image rows, converting into sprite format + for (int y = 0; y < height; y++) + { + png_bytep row = row_pointers[y]; + for (int x = 0; x < width; x++) + { + png_bytep px = &(row[x * 4]); + SetPixel(x, y, Pixel(px[0], px[1], px[2], px[3])); + } + } + + fclose(f); + return olc::OK; + + fail_load: + width = 0; + height = 0; + fclose(f); + pColData = nullptr; + return olc::FAIL; +#endif + } + + void Sprite::SetSampleMode(olc::Sprite::Mode mode) + { + modeSample = mode; + } + + + Pixel Sprite::GetPixel(int32_t x, int32_t y) + { + if (modeSample == olc::Sprite::Mode::NORMAL) + { + if (x >= 0 && x < width && y >= 0 && y < height) + return pColData[y*width + x]; + else + return Pixel(0, 0, 0, 0); + } + else + { + return pColData[abs(y%height)*width + abs(x%width)]; + } + } + + void Sprite::SetPixel(int32_t x, int32_t y, Pixel p) + { + +#ifdef OLC_DBG_OVERDRAW + nOverdrawCount++; +#endif + + if (x >= 0 && x < width && y >= 0 && y < height) + pColData[y*width + x] = p; + } + + Pixel Sprite::Sample(float x, float y) + { + int32_t sx = (int32_t)((x * (float)width) - 0.5f); + int32_t sy = (int32_t)((y * (float)height) - 0.5f); + return GetPixel(sx, sy); + } + + Pixel* Sprite::GetData() { return pColData; } + + //========================================================== + + ResourcePack::ResourcePack() + { + + } + + ResourcePack::~ResourcePack() + { + ClearPack(); + } + + olc::rcode ResourcePack::AddToPack(std::string sFile) + { + std::ifstream ifs(sFile, std::ifstream::binary); + if (!ifs.is_open()) return olc::FAIL; + + // Get File Size + std::streampos p = 0; + p = ifs.tellg(); + ifs.seekg(0, std::ios::end); + p = ifs.tellg() - p; + ifs.seekg(0, std::ios::beg); + + // Create entry + sEntry e; + e.data = nullptr; + e.nFileSize = (uint32_t)p; + + // Read file into memory + e.data = new uint8_t[(uint32_t)e.nFileSize]; + ifs.read((char*)e.data, e.nFileSize); + ifs.close(); + + // Add To Map + mapFiles[sFile] = e; + return olc::OK; + } + + olc::rcode ResourcePack::SavePack(std::string sFile) + { + std::ofstream ofs(sFile, std::ofstream::binary); + if (!ofs.is_open()) return olc::FAIL; + + // 1) Write Map + size_t nMapSize = mapFiles.size(); + ofs.write((char*)&nMapSize, sizeof(size_t)); + for (auto &e : mapFiles) + { + size_t nPathSize = e.first.size(); + ofs.write((char*)&nPathSize, sizeof(size_t)); + ofs.write(e.first.c_str(), nPathSize); + ofs.write((char*)&e.second.nID, sizeof(uint32_t)); + ofs.write((char*)&e.second.nFileSize, sizeof(uint32_t)); + ofs.write((char*)&e.second.nFileOffset, sizeof(uint32_t)); + } + + // 2) Write Data + std::streampos offset = ofs.tellp(); + for (auto &e : mapFiles) + { + e.second.nFileOffset = (uint32_t)offset; + ofs.write((char*)e.second.data, e.second.nFileSize); + offset += e.second.nFileSize; + } + + // 3) Rewrite Map (it has been updated with offsets now) + ofs.seekp(std::ios::beg); + ofs.write((char*)&nMapSize, sizeof(size_t)); + for (auto &e : mapFiles) + { + size_t nPathSize = e.first.size(); + ofs.write((char*)&nPathSize, sizeof(size_t)); + ofs.write(e.first.c_str(), nPathSize); + ofs.write((char*)&e.second.nID, sizeof(uint32_t)); + ofs.write((char*)&e.second.nFileSize, sizeof(uint32_t)); + ofs.write((char*)&e.second.nFileOffset, sizeof(uint32_t)); + } + ofs.close(); + + return olc::OK; + } + + olc::rcode ResourcePack::LoadPack(std::string sFile) + { + std::ifstream ifs(sFile, std::ifstream::binary); + if (!ifs.is_open()) return olc::FAIL; + + // 1) Read Map + size_t nMapEntries; + ifs.read((char*)&nMapEntries, sizeof(size_t)); + for (size_t i = 0; i < nMapEntries; i++) + { + size_t nFilePathSize = 0; + ifs.read((char*)&nFilePathSize, sizeof(size_t)); + + std::string sFileName(nFilePathSize, ' '); + for (size_t j = 0; j < nFilePathSize; j++) + sFileName[j] = ifs.get(); + + sEntry e; + e.data = nullptr; + ifs.read((char*)&e.nID, sizeof(uint32_t)); + ifs.read((char*)&e.nFileSize, sizeof(uint32_t)); + ifs.read((char*)&e.nFileOffset, sizeof(uint32_t)); + mapFiles[sFileName] = e; + } + + // 2) Read Data + for (auto &e : mapFiles) + { + e.second.data = new uint8_t[(uint32_t)e.second.nFileSize]; + ifs.seekg(e.second.nFileOffset); + ifs.read((char*)e.second.data, e.second.nFileSize); + e.second._config(); + } + + ifs.close(); + return olc::OK; + } + + olc::ResourcePack::sEntry ResourcePack::GetStreamBuffer(std::string sFile) + { + return mapFiles[sFile]; + } + + olc::rcode ResourcePack::ClearPack() + { + for (auto &e : mapFiles) + { + if (e.second.data != nullptr) + delete[] e.second.data; + } + + mapFiles.clear(); + return olc::OK; + } + + //========================================================== + + PixelGameEngine::PixelGameEngine() + { + sAppName = "Undefined"; + olc::PGEX::pge = this; + } + + olc::rcode PixelGameEngine::Construct(uint32_t screen_w, uint32_t screen_h, uint32_t pixel_w, uint32_t pixel_h) + { + nScreenWidth = screen_w; + nScreenHeight = screen_h; + nPixelWidth = pixel_w; + nPixelHeight = pixel_h; + + fPixelX = 2.0f / (float)(nScreenWidth); + fPixelY = 2.0f / (float)(nScreenHeight); + + if (nPixelWidth == 0 || nPixelHeight == 0 || nScreenWidth == 0 || nScreenHeight == 0) + return olc::FAIL; + +#ifdef _WIN32 +#ifdef UNICODE +#ifndef __MINGW32__ + wsAppName = ConvertS2W(sAppName); +#endif +#endif +#endif + // Load the default font sheet + olc_ConstructFontSheet(); + + // Create a sprite that represents the primary drawing target + pDefaultDrawTarget = new Sprite(nScreenWidth, nScreenHeight); + SetDrawTarget(nullptr); + return olc::OK; + } + + olc::rcode PixelGameEngine::Start() + { + // Construct the window + if (!olc_WindowCreate()) + return olc::FAIL; + + // Load libraries required for PNG file interaction +#ifdef _WIN32 + // Windows use GDI+ + Gdiplus::GdiplusStartupInput startupInput; + ULONG_PTR token; + Gdiplus::GdiplusStartup(&token, &startupInput, NULL); +#else + // Linux use libpng + +#endif + // Start the thread + bAtomActive = true; + std::thread t = std::thread(&PixelGameEngine::EngineThread, this); + +#ifdef _WIN32 + // Handle Windows Message Loop + MSG msg; + while (GetMessage(&msg, NULL, 0, 0) > 0) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +#endif + + // Wait for thread to be exited + t.join(); + return olc::OK; + } + + void PixelGameEngine::SetDrawTarget(Sprite *target) + { + if (target) + pDrawTarget = target; + else + pDrawTarget = pDefaultDrawTarget; + } + + Sprite* PixelGameEngine::GetDrawTarget() + { + return pDrawTarget; + } + + int32_t PixelGameEngine::GetDrawTargetWidth() + { + if (pDrawTarget) + return pDrawTarget->width; + else + return 0; + } + + int32_t PixelGameEngine::GetDrawTargetHeight() + { + if (pDrawTarget) + return pDrawTarget->height; + else + return 0; + } + + bool PixelGameEngine::IsFocused() + { + return bHasInputFocus; + } + + HWButton PixelGameEngine::GetKey(Key k) + { + return pKeyboardState[k]; + } + + HWButton PixelGameEngine::GetMouse(uint32_t b) + { + return pMouseState[b]; + } + + int32_t PixelGameEngine::GetMouseX() + { + return nMousePosX; + } + + int32_t PixelGameEngine::GetMouseY() + { + return nMousePosY; + } + + int32_t PixelGameEngine::ScreenWidth() + { + return nScreenWidth; + } + + int32_t PixelGameEngine::ScreenHeight() + { + return nScreenHeight; + } + + void PixelGameEngine::Draw(int32_t x, int32_t y, Pixel p) + { + if (!pDrawTarget) return; + + + if (nPixelMode == Pixel::NORMAL) + { + pDrawTarget->SetPixel(x, y, p); + return; + } + + if (nPixelMode == Pixel::MASK) + { + if(p.a == 255) + pDrawTarget->SetPixel(x, y, p); + return; + } + + if (nPixelMode == Pixel::ALPHA) + { + Pixel d = pDrawTarget->GetPixel(x, y); + float a = (float)(p.a / 255.0f) * fBlendFactor; + float c = 1.0f - a; + float r = a * (float)p.r + c * (float)d.r; + float g = a * (float)p.g + c * (float)d.g; + float b = a * (float)p.b + c * (float)d.b; + pDrawTarget->SetPixel(x, y, Pixel((uint8_t)r, (uint8_t)g, (uint8_t)b)); + return; + } + + if (nPixelMode == Pixel::CUSTOM) + { + pDrawTarget->SetPixel(x, y, funcPixelMode(x, y, p, pDrawTarget->GetPixel(x, y))); + return; + } + } + + void PixelGameEngine::SetSubPixelOffset(float ox, float oy) + { + fSubPixelOffsetX = ox * fPixelX; + fSubPixelOffsetY = oy * fPixelY; + } + + void PixelGameEngine::DrawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, Pixel p) + { + int x, y, dx, dy, dx1, dy1, px, py, xe, ye, i; + dx = x2 - x1; dy = y2 - y1; + + // straight lines idea by gurkanctn + if (dx == 0) // Line is vertical + { + if (y2 < y1) std::swap(y1, y2); + for (y = y1; y <= y2; y++) + Draw(x1, y, p); + return; + } + + if (dy == 0) // Line is horizontal + { + if (x2 < x1) std::swap(x1, x2); + for (x = x1; x <= x2; x++) + Draw(x, y1, p); + return; + } + + // Line is Funk-aye + dx1 = abs(dx); dy1 = abs(dy); + px = 2 * dy1 - dx1; py = 2 * dx1 - dy1; + if (dy1 <= dx1) + { + if (dx >= 0) + { + x = x1; y = y1; xe = x2; + } + else + { + x = x2; y = y2; xe = x1; + } + + Draw(x, y, p); + + for (i = 0; x0 && dy>0)) y = y + 1; else y = y - 1; + px = px + 2 * (dy1 - dx1); + } + Draw(x, y, p); + } + } + else + { + if (dy >= 0) + { + x = x1; y = y1; ye = y2; + } + else + { + x = x2; y = y2; ye = y1; + } + + Draw(x, y, p); + + for (i = 0; y0 && dy>0)) x = x + 1; else x = x - 1; + py = py + 2 * (dx1 - dy1); + } + Draw(x, y, p); + } + } + } + + void PixelGameEngine::DrawCircle(int32_t x, int32_t y, int32_t radius, Pixel p) + { + int x0 = 0; + int y0 = radius; + int d = 3 - 2 * radius; + if (!radius) return; + + while (y0 >= x0) // only formulate 1/8 of circle + { + Draw(x - x0, y - y0, p);//upper left left + Draw(x - y0, y - x0, p);//upper upper left + Draw(x + y0, y - x0, p);//upper upper right + Draw(x + x0, y - y0, p);//upper right right + Draw(x - x0, y + y0, p);//lower left left + Draw(x - y0, y + x0, p);//lower lower left + Draw(x + y0, y + x0, p);//lower lower right + Draw(x + x0, y + y0, p);//lower right right + if (d < 0) d += 4 * x0++ + 6; + else d += 4 * (x0++ - y0--) + 10; + } + } + + void PixelGameEngine::FillCircle(int32_t x, int32_t y, int32_t radius, Pixel p) + { + // Taken from wikipedia + int x0 = 0; + int y0 = radius; + int d = 3 - 2 * radius; + if (!radius) return; + + auto drawline = [&](int sx, int ex, int ny) + { + for (int i = sx; i <= ex; i++) + Draw(i, ny, p); + }; + + while (y0 >= x0) + { + // Modified to draw scan-lines instead of edges + drawline(x - x0, x + x0, y - y0); + drawline(x - y0, x + y0, y - x0); + drawline(x - x0, x + x0, y + y0); + drawline(x - y0, x + y0, y + x0); + if (d < 0) d += 4 * x0++ + 6; + else d += 4 * (x0++ - y0--) + 10; + } + } + + void PixelGameEngine::DrawRect(int32_t x, int32_t y, int32_t w, int32_t h, Pixel p) + { + DrawLine(x, y, x+w, y, p); + DrawLine(x+w, y, x+w, y+h, p); + DrawLine(x+w, y+h, x, y+h, p); + DrawLine(x, y+h, x, y, p); + } + + void PixelGameEngine::Clear(Pixel p) + { + int pixels = GetDrawTargetWidth() * GetDrawTargetHeight(); + Pixel* m = GetDrawTarget()->GetData(); + for (int i = 0; i < pixels; i++) + m[i] = p; +#ifdef OLC_DBG_OVERDRAW + olc::Sprite::nOverdrawCount += pixels; +#endif + } + + void PixelGameEngine::FillRect(int32_t x, int32_t y, int32_t w, int32_t h, Pixel p) + { + int32_t x2 = x + w; + int32_t y2 = y + h; + + if (x < 0) x = 0; + if (x >= (int32_t)nScreenWidth) x = (int32_t)nScreenWidth; + if (y < 0) y = 0; + if (y >= (int32_t)nScreenHeight) y = (int32_t)nScreenHeight; + + if (x2 < 0) x2 = 0; + if (x2 >= (int32_t)nScreenWidth) x2 = (int32_t)nScreenWidth; + if (y2 < 0) y2 = 0; + if (y2 >= (int32_t)nScreenHeight) y2 = (int32_t)nScreenHeight; + + for (int i = x; i < x2; i++) + for (int j = y; j < y2; j++) + Draw(i, j, p); + } + + void PixelGameEngine::DrawTriangle(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, Pixel p) + { + DrawLine(x1, y1, x2, y2, p); + DrawLine(x2, y2, x3, y3, p); + DrawLine(x3, y3, x1, y1, p); + } + + // https://www.avrfreaks.net/sites/default/files/triangles.c + void PixelGameEngine::FillTriangle(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, Pixel p) + { + auto SWAP = [](int &x, int &y) { int t = x; x = y; y = t; }; + auto drawline = [&](int sx, int ex, int ny) { for (int i = sx; i <= ex; i++) Draw(i, ny, p); }; + + int t1x, t2x, y, minx, maxx, t1xp, t2xp; + bool changed1 = false; + bool changed2 = false; + int signx1, signx2, dx1, dy1, dx2, dy2; + int e1, e2; + // Sort vertices + if (y1>y2) { SWAP(y1, y2); SWAP(x1, x2); } + if (y1>y3) { SWAP(y1, y3); SWAP(x1, x3); } + if (y2>y3) { SWAP(y2, y3); SWAP(x2, x3); } + + t1x = t2x = x1; y = y1; // Starting points + dx1 = (int)(x2 - x1); if (dx1<0) { dx1 = -dx1; signx1 = -1; } + else signx1 = 1; + dy1 = (int)(y2 - y1); + + dx2 = (int)(x3 - x1); if (dx2<0) { dx2 = -dx2; signx2 = -1; } + else signx2 = 1; + dy2 = (int)(y3 - y1); + + if (dy1 > dx1) { // swap values + SWAP(dx1, dy1); + changed1 = true; + } + if (dy2 > dx2) { // swap values + SWAP(dy2, dx2); + changed2 = true; + } + + e2 = (int)(dx2 >> 1); + // Flat top, just process the second half + if (y1 == y2) goto next; + e1 = (int)(dx1 >> 1); + + for (int i = 0; i < dx1;) { + t1xp = 0; t2xp = 0; + if (t1x= dx1) { + e1 -= dx1; + if (changed1) t1xp = signx1;//t1x += signx1; + else goto next1; + } + if (changed1) break; + else t1x += signx1; + } + // Move line + next1: + // process second line until y value is about to change + while (1) { + e2 += dy2; + while (e2 >= dx2) { + e2 -= dx2; + if (changed2) t2xp = signx2;//t2x += signx2; + else goto next2; + } + if (changed2) break; + else t2x += signx2; + } + next2: + if (minx>t1x) minx = t1x; if (minx>t2x) minx = t2x; + if (maxx dx1) { // swap values + SWAP(dy1, dx1); + changed1 = true; + } + else changed1 = false; + + e1 = (int)(dx1 >> 1); + + for (int i = 0; i <= dx1; i++) { + t1xp = 0; t2xp = 0; + if (t1x= dx1) { + e1 -= dx1; + if (changed1) { t1xp = signx1; break; }//t1x += signx1; + else goto next3; + } + if (changed1) break; + else t1x += signx1; + if (i= dx2) { + e2 -= dx2; + if (changed2) t2xp = signx2; + else goto next4; + } + if (changed2) break; + else t2x += signx2; + } + next4: + + if (minx>t1x) minx = t1x; if (minx>t2x) minx = t2x; + if (maxxy3) return; + } + } + + void PixelGameEngine::DrawSprite(int32_t x, int32_t y, Sprite *sprite, uint32_t scale) + { + if (sprite == nullptr) + return; + + if (scale > 1) + { + for (int32_t i = 0; i < sprite->width; i++) + for (int32_t j = 0; j < sprite->height; j++) + for (uint32_t is = 0; is < scale; is++) + for (uint32_t js = 0; js < scale; js++) + Draw(x + (i*scale) + is, y + (j*scale) + js, sprite->GetPixel(i, j)); + } + else + { + for (int32_t i = 0; i < sprite->width; i++) + for (int32_t j = 0; j < sprite->height; j++) + Draw(x + i, y + j, sprite->GetPixel(i, j)); + } + } + + void PixelGameEngine::DrawPartialSprite(int32_t x, int32_t y, Sprite *sprite, int32_t ox, int32_t oy, int32_t w, int32_t h, uint32_t scale) + { + if (sprite == nullptr) + return; + + if (scale > 1) + { + for (int32_t i = 0; i < w; i++) + for (int32_t j = 0; j < h; j++) + for (uint32_t is = 0; is < scale; is++) + for (uint32_t js = 0; js < scale; js++) + Draw(x + (i*scale) + is, y + (j*scale) + js, sprite->GetPixel(i + ox, j + oy)); + } + else + { + for (int32_t i = 0; i < w; i++) + for (int32_t j = 0; j < h; j++) + Draw(x + i, y + j, sprite->GetPixel(i + ox, j + oy)); + } + } + + void PixelGameEngine::DrawString(int32_t x, int32_t y, std::string sText, Pixel col, uint32_t scale) + { + int32_t sx = 0; + int32_t sy = 0; + Pixel::Mode m = nPixelMode; + if(col.ALPHA != 255) SetPixelMode(Pixel::ALPHA); + else SetPixelMode(Pixel::MASK); + for (auto c : sText) + { + if (c == '\n') + { + sx = 0; sy += 8 * scale; + } + else + { + int32_t ox = (c - 32) % 16; + int32_t oy = (c - 32) / 16; + + if (scale > 1) + { + for (uint32_t i = 0; i < 8; i++) + for (uint32_t j = 0; j < 8; j++) + if (fontSprite->GetPixel(i + ox * 8, j + oy * 8).r > 0) + for (uint32_t is = 0; is < scale; is++) + for (uint32_t js = 0; js < scale; js++) + Draw(x + sx + (i*scale) + is, y + sy + (j*scale) + js, col); + } + else + { + for (uint32_t i = 0; i < 8; i++) + for (uint32_t j = 0; j < 8; j++) + if (fontSprite->GetPixel(i + ox * 8, j + oy * 8).r > 0) + Draw(x + sx + i, y + sy + j, col); + } + sx += 8 * scale; + } + } + SetPixelMode(m); + } + + void PixelGameEngine::SetPixelMode(Pixel::Mode m) + { + nPixelMode = m; + } + + Pixel::Mode PixelGameEngine::GetPixelMode() + { + return nPixelMode; + } + + void PixelGameEngine::SetPixelMode(std::function pixelMode) + { + funcPixelMode = pixelMode; + nPixelMode = Pixel::Mode::CUSTOM; + } + + void PixelGameEngine::SetPixelBlend(float fBlend) + { + fBlendFactor = fBlend; + if (fBlendFactor < 0.0f) fBlendFactor = 0.0f; + if (fBlendFactor > 1.0f) fBlendFactor = 1.0f; + } + + // User must override these functions as required. I have not made + // them abstract because I do need a default behaviour to occur if + // they are not overwritten + bool PixelGameEngine::OnUserCreate() + { return false; } + bool PixelGameEngine::OnUserUpdate(float fElapsedTime) + { return false; } + bool PixelGameEngine::OnUserDestroy() + { return true; } + ////////////////////////////////////////////////////////////////// + + void PixelGameEngine::olc_UpdateMouse(int32_t x, int32_t y) + { + // Mouse coords come in screen space + // But leave in pixel space + nMousePosX = x / (int32_t)nPixelWidth; + nMousePosY = y / (int32_t)nPixelHeight; + + if (nMousePosX >= (int32_t)nScreenWidth) + nMousePosX = nScreenWidth - 1; + if (nMousePosY >= (int32_t)nScreenHeight) + nMousePosY = nScreenHeight - 1; + + if (nMousePosX < 0) + nMousePosX = 0; + if (nMousePosY < 0) + nMousePosY = 0; + } + + void PixelGameEngine::EngineThread() + { + // Start OpenGL, the context is owned by the game thread + olc_OpenGLCreate(); + + // Create Screen Texture - disable filtering + glEnable(GL_TEXTURE_2D); + glGenTextures(1, &glBuffer); + glBindTexture(GL_TEXTURE_2D, glBuffer); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, nScreenWidth, nScreenHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, pDefaultDrawTarget->GetData()); + + + // Create user resources as part of this thread + if (!OnUserCreate()) + bAtomActive = false; + + auto tp1 = std::chrono::system_clock::now(); + auto tp2 = std::chrono::system_clock::now(); + + while (bAtomActive) + { + // Run as fast as possible + while (bAtomActive) + { + // Handle Timing + tp2 = std::chrono::system_clock::now(); + std::chrono::duration elapsedTime = tp2 - tp1; + tp1 = tp2; + + // Our time per frame coefficient + float fElapsedTime = elapsedTime.count(); + +#ifndef _WIN32 + // Handle Xlib Message Loop - we do this in the + // same thread that OpenGL was created so we dont + // need to worry too much about multithreading with X11 + XEvent xev; + while (XPending(olc_Display)) + { + XNextEvent(olc_Display, &xev); + if (xev.type == Expose) + { + XWindowAttributes gwa; + XGetWindowAttributes(olc_Display, olc_Window, &gwa); + glViewport(0, 0, gwa.width, gwa.height); + } + else if (xev.type == KeyPress) + { + KeySym sym = XLookupKeysym(&xev.xkey, 0); + pKeyNewState[mapKeys[sym]] = true; + } + else if (xev.type == KeyRelease) + { + KeySym sym = XLookupKeysym(&xev.xkey, 0); + pKeyNewState[mapKeys[sym]] = false; + } + else if (xev.type == ButtonPress) + { + pMouseNewState[xev.xbutton.button-1] = true; + } + else if (xev.type == ButtonRelease) + { + pMouseNewState[xev.xbutton.button-1] = false; + } + else if (xev.type == MotionNotify) + { + olc_UpdateMouse(xev.xmotion.x, xev.xmotion.y); + } + else if (xev.type == FocusIn) + { + bHasInputFocus = true; + } + else if (xev.type == FocusOut) + { + bHasInputFocus = false; + } + else if (xev.type == ClientMessage) + { + bAtomActive = false; + } + } +#endif + + // Handle User Input - Keyboard + for (int i = 0; i < 256; i++) + { + pKeyboardState[i].bPressed = false; + pKeyboardState[i].bReleased = false; + + if (pKeyNewState[i] != pKeyOldState[i]) + { + if (pKeyNewState[i]) + { + pKeyboardState[i].bPressed = !pKeyboardState[i].bHeld; + pKeyboardState[i].bHeld = true; + } + else + { + pKeyboardState[i].bReleased = true; + pKeyboardState[i].bHeld = false; + } + } + + pKeyOldState[i] = pKeyNewState[i]; + } + + // Handle User Input - Mouse + for (int i = 0; i < 5; i++) + { + pMouseState[i].bPressed = false; + pMouseState[i].bReleased = false; + + if (pMouseNewState[i] != pMouseOldState[i]) + { + if (pMouseNewState[i]) + { + pMouseState[i].bPressed = !pMouseState[i].bHeld; + pMouseState[i].bHeld = true; + } + else + { + pMouseState[i].bReleased = true; + pMouseState[i].bHeld = false; + } + } + + pMouseOldState[i] = pMouseNewState[i]; + } + +#ifdef OLC_DBG_OVERDRAW + olc::Sprite::nOverdrawCount = 0; +#endif + + // Handle Frame Update + if (!OnUserUpdate(fElapsedTime)) + bAtomActive = false; + + // Display Graphics + + // TODO: This is a bit slow (especially in debug, but 100x faster in release mode???) + // Copy pixel array into texture + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, nScreenWidth, nScreenHeight, GL_RGBA, GL_UNSIGNED_BYTE, pDefaultDrawTarget->GetData()); + + // Display texture on screen + glBegin(GL_QUADS); + glTexCoord2f(0.0, 1.0); glVertex3f(-1.0f + (fSubPixelOffsetX), -1.0f + (fSubPixelOffsetY), 0.0f); + glTexCoord2f(0.0, 0.0); glVertex3f(-1.0f + (fSubPixelOffsetX), 1.0f + (fSubPixelOffsetY), 0.0f); + glTexCoord2f(1.0, 0.0); glVertex3f( 1.0f + (fSubPixelOffsetX), 1.0f + (fSubPixelOffsetY), 0.0f); + glTexCoord2f(1.0, 1.0); glVertex3f( 1.0f + (fSubPixelOffsetX), -1.0f + (fSubPixelOffsetY), 0.0f); + glEnd(); + + // Present Graphics to screen +#ifdef _WIN32 + SwapBuffers(glDeviceContext); +#else + glXSwapBuffers(olc_Display, olc_Window); +#endif + + // Update Title Bar + fFrameTimer += fElapsedTime; + nFrameCount++; + if (fFrameTimer >= 1.0f) + { + fFrameTimer -= 1.0f; + + std::string sTitle = "OneLoneCoder.com - Pixel Game Engine - " + sAppName + " - FPS: " + std::to_string(nFrameCount); +#ifdef _WIN32 +#ifdef UNICODE + SetWindowText(olc_hWnd, ConvertS2W(sTitle).c_str()); +#else + SetWindowText(olc_hWnd, sTitle.c_str()); +#endif +#else + XStoreName(olc_Display, olc_Window, sTitle.c_str()); +#endif + nFrameCount = 0; + } + } + + // Allow the user to free resources if they have overrided the destroy function + if (OnUserDestroy()) + { + // User has permitted destroy, so exit and clean up + } + else + { + // User denied destroy for some reason, so continue running + bAtomActive = true; + } + } + +#ifdef _WIN32 + wglDeleteContext(glRenderContext); + PostMessage(olc_hWnd, WM_DESTROY, 0, 0); +#else + glXMakeCurrent(olc_Display, None, NULL); + glXDestroyContext(olc_Display, glDeviceContext); + XDestroyWindow(olc_Display, olc_Window); + XCloseDisplay(olc_Display); +#endif + + } + + + void PixelGameEngine::olc_ConstructFontSheet() + { + std::string data; + data += "?Q`0001oOch0o01o@F40o000000000"; + data += "O000000nOT0063Qo4d8>?7a14Gno94AA4gno94AaOT0>o3`oO400o7QN00000400"; + data += "Of80001oOg<7O7moBGT7O7lABET024@aBEd714AiOdl717a_=TH013Q>00000000"; + data += "720D000V?V5oB3Q_HdUoE7a9@DdDE4A9@DmoE4A;Hg]oM4Aj8S4D84@`00000000"; + data += "OaPT1000Oa`^13P1@AI[?g`1@A=[OdAoHgljA4Ao?WlBA7l1710007l100000000"; + data += "ObM6000oOfMV?3QoBDD`O7a0BDDH@5A0BDD<@5A0BGeVO5ao@CQR?5Po00000000"; + data += "Oc``000?Ogij70PO2D]??0Ph2DUM@7i`2DTg@7lh2GUj?0TO0C1870T?00000000"; + data += "70<4001o?P<7?1QoHg43O;`h@GT0@:@LB@d0>:@hN@L0@?aoN@<0O7ao0000?000"; + data += "OcH0001SOglLA7mg24TnK7ln24US>0PL24U140PnOgl0>7QgOcH0K71S0000A000"; + data += "00H00000@Dm1S007@DUSg00?OdTnH7YhOfTL<7Yh@Cl0700?@Ah0300700000000"; + data += "<008001QL00ZA41a@6HnI<1i@FHLM81M@@0LG81?O`0nC?Y7?`0ZA7Y300080000"; + data += "O`082000Oh0827mo6>Hn?Wmo?6HnMb11MP08@C11H`08@FP0@@0004@000000000"; + data += "00P00001Oab00003OcKP0006@6=PMgl<@440MglH@000000`@000001P00000000"; + data += "Ob@8@@00Ob@8@Ga13R@8Mga172@8?PAo3R@827QoOb@820@0O`0007`0000007P0"; + data += "O`000P08Od400g`<3V=P0G`673IP0`@3>1`00P@6O`P00g`SetPixel(px, py, olc::Pixel(k, k, k, k)); + if (++py == 48) { px++; py = 0; } + } + } + } + +#ifdef _WIN32 + HWND PixelGameEngine::olc_WindowCreate() + { + WNDCLASS wc; + wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; + wc.hInstance = GetModuleHandle(nullptr); + wc.lpfnWndProc = olc_WindowEvent; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.lpszMenuName = nullptr; + wc.hbrBackground = nullptr; +#ifdef UNICODE + wc.lpszClassName = L"OLC_PIXEL_GAME_ENGINE"; +#else + wc.lpszClassName = "OLC_PIXEL_GAME_ENGINE"; +#endif + + RegisterClass(&wc); + + // Define window furniture + DWORD dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; + DWORD dwStyle = WS_CAPTION | WS_SYSMENU | WS_VISIBLE; + RECT rWndRect = { 0, 0, (LONG)nScreenWidth * (LONG)nPixelWidth, (LONG)nScreenHeight * (LONG)nPixelHeight }; + + // Keep client size as requested + AdjustWindowRectEx(&rWndRect, dwStyle, FALSE, dwExStyle); + + int width = rWndRect.right - rWndRect.left; + int height = rWndRect.bottom - rWndRect.top; + +#ifdef UNICODE + olc_hWnd = CreateWindowEx(dwExStyle, L"OLC_PIXEL_GAME_ENGINE", L"", dwStyle, + 30, 30, width, height, NULL, NULL, GetModuleHandle(nullptr), this); +#else + olc_hWnd = CreateWindowEx(dwExStyle, "OLC_PIXEL_GAME_ENGINE", "", dwStyle, + 30, 30, width, height, NULL, NULL, GetModuleHandle(nullptr), this); +#endif + + // Create Keyboard Mapping + mapKeys[0x41] = Key::A; mapKeys[0x42] = Key::B; mapKeys[0x43] = Key::C; mapKeys[0x44] = Key::D; mapKeys[0x45] = Key::E; + mapKeys[0x46] = Key::F; mapKeys[0x47] = Key::G; mapKeys[0x48] = Key::H; mapKeys[0x49] = Key::I; mapKeys[0x4A] = Key::J; + mapKeys[0x4B] = Key::K; mapKeys[0x4C] = Key::L; mapKeys[0x4D] = Key::M; mapKeys[0x4E] = Key::N; mapKeys[0x4F] = Key::O; + mapKeys[0x50] = Key::P; mapKeys[0x51] = Key::Q; mapKeys[0x52] = Key::R; mapKeys[0x53] = Key::S; mapKeys[0x54] = Key::T; + mapKeys[0x55] = Key::U; mapKeys[0x56] = Key::V; mapKeys[0x57] = Key::W; mapKeys[0x58] = Key::X; mapKeys[0x59] = Key::Y; + mapKeys[0x5A] = Key::Z; + + mapKeys[VK_F1] = Key::F1; mapKeys[VK_F2] = Key::F2; mapKeys[VK_F3] = Key::F3; mapKeys[VK_F4] = Key::F4; + mapKeys[VK_F5] = Key::F5; mapKeys[VK_F6] = Key::F6; mapKeys[VK_F7] = Key::F7; mapKeys[VK_F8] = Key::F8; + mapKeys[VK_F9] = Key::F9; mapKeys[VK_F10] = Key::F10; mapKeys[VK_F11] = Key::F11; mapKeys[VK_F12] = Key::F12; + + mapKeys[VK_DOWN] = Key::DOWN; mapKeys[VK_LEFT] = Key::LEFT; mapKeys[VK_RIGHT] = Key::RIGHT; mapKeys[VK_UP] = Key::UP; + mapKeys[VK_RETURN] = Key::ENTER; //mapKeys[VK_RETURN] = Key::RETURN; + + mapKeys[VK_BACK] = Key::BACK; mapKeys[VK_ESCAPE] = Key::ESCAPE; mapKeys[VK_RETURN] = Key::ENTER; mapKeys[VK_PAUSE] = Key::PAUSE; + mapKeys[VK_SCROLL] = Key::SCROLL; mapKeys[VK_TAB] = Key::TAB; mapKeys[VK_DELETE] = Key::DEL; mapKeys[VK_HOME] = Key::HOME; + mapKeys[VK_END] = Key::END; mapKeys[VK_PRIOR] = Key::PGUP; mapKeys[VK_NEXT] = Key::PGDN; mapKeys[VK_INSERT] = Key::INS; + mapKeys[VK_SHIFT] = Key::SHIFT; mapKeys[VK_CONTROL] = Key::CTRL; + mapKeys[VK_SPACE] = Key::SPACE; + + mapKeys[0x30] = Key::K0; mapKeys[0x31] = Key::K1; mapKeys[0x32] = Key::K2; mapKeys[0x33] = Key::K3; mapKeys[0x34] = Key::K4; + mapKeys[0x35] = Key::K5; mapKeys[0x36] = Key::K6; mapKeys[0x37] = Key::K7; mapKeys[0x38] = Key::K8; mapKeys[0x39] = Key::K9; + + mapKeys[VK_NUMPAD0] = Key::NP0; mapKeys[VK_NUMPAD1] = Key::NP1; mapKeys[VK_NUMPAD2] = Key::NP2; mapKeys[VK_NUMPAD3] = Key::NP3; mapKeys[VK_NUMPAD4] = Key::NP4; + mapKeys[VK_NUMPAD5] = Key::NP5; mapKeys[VK_NUMPAD6] = Key::NP6; mapKeys[VK_NUMPAD7] = Key::NP7; mapKeys[VK_NUMPAD8] = Key::NP8; mapKeys[VK_NUMPAD9] = Key::NP9; + mapKeys[VK_MULTIPLY] = Key::NP_MUL; mapKeys[VK_ADD] = Key::NP_ADD; mapKeys[VK_DIVIDE] = Key::NP_DIV; mapKeys[VK_SUBTRACT] = Key::NP_SUB; mapKeys[VK_DECIMAL] = Key::NP_DECIMAL; + + return olc_hWnd; + } + + bool PixelGameEngine::olc_OpenGLCreate() + { + // Create Device Context + glDeviceContext = GetDC(olc_hWnd); + PIXELFORMATDESCRIPTOR pfd = + { + sizeof(PIXELFORMATDESCRIPTOR), 1, + PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, + PFD_TYPE_RGBA, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + PFD_MAIN_PLANE, 0, 0, 0, 0 + }; + + int pf = 0; + if (!(pf = ChoosePixelFormat(glDeviceContext, &pfd))) return false; + SetPixelFormat(glDeviceContext, pf, &pfd); + + if (!(glRenderContext = wglCreateContext(glDeviceContext))) return false; + wglMakeCurrent(glDeviceContext, glRenderContext); + + // Remove Frame cap + wglSwapInterval = (wglSwapInterval_t*)wglGetProcAddress("wglSwapIntervalEXT"); + wglSwapInterval(0); + return true; + } + + // Windows Event Handler + LRESULT CALLBACK PixelGameEngine::olc_WindowEvent(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) + { + static PixelGameEngine *sge; + switch (uMsg) + { + case WM_CREATE: sge = (PixelGameEngine*)((LPCREATESTRUCT)lParam)->lpCreateParams; return 0; + case WM_MOUSEMOVE: + { + uint16_t x = lParam & 0xFFFF; // Thanks @ForAbby (Discord) + uint16_t y = (lParam >> 16) & 0xFFFF; + int16_t ix = *(int16_t*)&x; + int16_t iy = *(int16_t*)&y; + sge->olc_UpdateMouse(ix, iy); + return 0; + } + case WM_MOUSELEAVE: sge->bHasMouseFocus = false; + case WM_SETFOCUS: sge->bHasInputFocus = true; return 0; + case WM_KILLFOCUS: sge->bHasInputFocus = false; return 0; + case WM_KEYDOWN: sge->pKeyNewState[mapKeys[wParam]] = true; return 0; + case WM_KEYUP: sge->pKeyNewState[mapKeys[wParam]] = false; return 0; + case WM_LBUTTONDOWN:sge->pMouseNewState[0] = true; return 0; + case WM_LBUTTONUP: sge->pMouseNewState[0] = false; return 0; + case WM_RBUTTONDOWN:sge->pMouseNewState[1] = true; return 0; + case WM_RBUTTONUP: sge->pMouseNewState[1] = false; return 0; + case WM_MBUTTONDOWN:sge->pMouseNewState[2] = true; return 0; + case WM_MBUTTONUP: sge->pMouseNewState[2] = false; return 0; + case WM_CLOSE: bAtomActive = false; return 0; + case WM_DESTROY: PostQuitMessage(0); return 0; + } + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } +#else + // Do the Linux stuff! + Display* PixelGameEngine::olc_WindowCreate() + { + XInitThreads(); + + // Grab the deafult display and window + olc_Display = XOpenDisplay(NULL); + olc_WindowRoot = DefaultRootWindow(olc_Display); + + // Based on the display capabilities, configure the appearance of the window + GLint olc_GLAttribs[] = { GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_DOUBLEBUFFER, None }; + olc_VisualInfo = glXChooseVisual(olc_Display, 0, olc_GLAttribs); + olc_ColourMap = XCreateColormap(olc_Display, olc_WindowRoot, olc_VisualInfo->visual, AllocNone); + olc_SetWindowAttribs.colormap = olc_ColourMap; + + // Register which events we are interested in receiving + olc_SetWindowAttribs.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask; + + // Create the window + olc_Window = XCreateWindow(olc_Display, olc_WindowRoot, 30, 30, nScreenWidth * nPixelWidth, nScreenHeight * nPixelHeight, 0, olc_VisualInfo->depth, InputOutput, olc_VisualInfo->visual, CWColormap | CWEventMask, &olc_SetWindowAttribs); + + Atom wmDelete = XInternAtom(olc_Display, "WM_DELETE_WINDOW", true); + XSetWMProtocols(olc_Display, olc_Window, &wmDelete, 1); + + XMapWindow(olc_Display, olc_Window); + XStoreName(olc_Display, olc_Window, "OneLoneCoder.com - Pixel Game Engine"); + + // Create Keyboard Mapping + mapKeys[0x61] = Key::A; mapKeys[0x62] = Key::B; mapKeys[0x63] = Key::C; mapKeys[0x64] = Key::D; mapKeys[0x65] = Key::E; + mapKeys[0x66] = Key::F; mapKeys[0x67] = Key::G; mapKeys[0x68] = Key::H; mapKeys[0x69] = Key::I; mapKeys[0x6A] = Key::J; + mapKeys[0x6B] = Key::K; mapKeys[0x6C] = Key::L; mapKeys[0x6D] = Key::M; mapKeys[0x6E] = Key::N; mapKeys[0x6F] = Key::O; + mapKeys[0x70] = Key::P; mapKeys[0x71] = Key::Q; mapKeys[0x72] = Key::R; mapKeys[0x73] = Key::S; mapKeys[0x74] = Key::T; + mapKeys[0x75] = Key::U; mapKeys[0x76] = Key::V; mapKeys[0x77] = Key::W; mapKeys[0x78] = Key::X; mapKeys[0x79] = Key::Y; + mapKeys[0x7A] = Key::Z; + + mapKeys[XK_F1] = Key::F1; mapKeys[XK_F2] = Key::F2; mapKeys[XK_F3] = Key::F3; mapKeys[XK_F4] = Key::F4; + mapKeys[XK_F5] = Key::F5; mapKeys[XK_F6] = Key::F6; mapKeys[XK_F7] = Key::F7; mapKeys[XK_F8] = Key::F8; + mapKeys[XK_F9] = Key::F9; mapKeys[XK_F10] = Key::F10; mapKeys[XK_F11] = Key::F11; mapKeys[XK_F12] = Key::F12; + + mapKeys[XK_Down] = Key::DOWN; mapKeys[XK_Left] = Key::LEFT; mapKeys[XK_Right] = Key::RIGHT; mapKeys[XK_Up] = Key::UP; + mapKeys[XK_KP_Enter] = Key::ENTER; mapKeys[XK_Return] = Key::ENTER; + + mapKeys[XK_BackSpace] = Key::BACK; mapKeys[XK_Escape] = Key::ESCAPE; mapKeys[XK_Linefeed] = Key::ENTER; mapKeys[XK_Pause] = Key::PAUSE; + mapKeys[XK_Scroll_Lock] = Key::SCROLL; mapKeys[XK_Tab] = Key::TAB; mapKeys[XK_Delete] = Key::DEL; mapKeys[XK_Home] = Key::HOME; + mapKeys[XK_End] = Key::END; mapKeys[XK_Page_Up] = Key::PGUP; mapKeys[XK_Page_Down] = Key::PGDN; mapKeys[XK_Insert] = Key::INS; + mapKeys[XK_Shift_L] = Key::SHIFT; mapKeys[XK_Shift_R] = Key::SHIFT; mapKeys[XK_Control_L] = Key::CTRL; mapKeys[XK_Control_R] = Key::CTRL; + mapKeys[XK_space] = Key::SPACE; + + mapKeys[XK_0] = Key::K0; mapKeys[XK_1] = Key::K1; mapKeys[XK_2] = Key::K2; mapKeys[XK_3] = Key::K3; mapKeys[XK_4] = Key::K4; + mapKeys[XK_5] = Key::K5; mapKeys[XK_6] = Key::K6; mapKeys[XK_7] = Key::K7; mapKeys[XK_8] = Key::K8; mapKeys[XK_9] = Key::K9; + + mapKeys[XK_KP_0] = Key::NP0; mapKeys[XK_KP_1] = Key::NP1; mapKeys[XK_KP_2] = Key::NP2; mapKeys[XK_KP_3] = Key::NP3; mapKeys[XK_KP_4] = Key::NP4; + mapKeys[XK_KP_5] = Key::NP5; mapKeys[XK_KP_6] = Key::NP6; mapKeys[XK_KP_7] = Key::NP7; mapKeys[XK_KP_8] = Key::NP8; mapKeys[XK_KP_9] = Key::NP9; + mapKeys[XK_KP_Multiply] = Key::NP_MUL; mapKeys[XK_KP_Add] = Key::NP_ADD; mapKeys[XK_KP_Divide] = Key::NP_DIV; mapKeys[XK_KP_Subtract] = Key::NP_SUB; mapKeys[XK_KP_Decimal] = Key::NP_DECIMAL; + + return olc_Display; + } + + bool PixelGameEngine::olc_OpenGLCreate() + { + glDeviceContext = glXCreateContext(olc_Display, olc_VisualInfo, nullptr, GL_TRUE); + glXMakeCurrent(olc_Display, olc_Window, glDeviceContext); + + XWindowAttributes gwa; + XGetWindowAttributes(olc_Display, olc_Window, &gwa); + glViewport(0, 0, gwa.width, gwa.height); + + glSwapIntervalEXT = nullptr; + glSwapIntervalEXT = (glSwapInterval_t*)glXGetProcAddress((unsigned char*)"glXSwapIntervalEXT"); + if (glSwapIntervalEXT) + glSwapIntervalEXT(olc_Display, olc_Window, 0); + else + { + printf("NOTE: Could not disable VSYNC, glXSwapIntervalEXT() was not found!\n"); + printf(" Don't worry though, things will still work, it's just the\n"); + printf(" frame rate will be capped to your monitors refresh rate - javidx9\n"); + } + + return true; + } + +#endif + + // Need a couple of statics as these are singleton instances + // read from multiple locations + std::atomic PixelGameEngine::bAtomActive{ false }; + std::map PixelGameEngine::mapKeys; + olc::PixelGameEngine* olc::PGEX::pge = nullptr; +#ifdef OLC_DBG_OVERDRAW + int olc::Sprite::nOverdrawCount = 0; +#endif + //============================================================= +} + +#endif \ No newline at end of file From d879b44021c32d09df34ebc8ff9d470bf6999a80 Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sun, 20 Jan 2019 00:08:05 +0000 Subject: [PATCH 07/12] Add files via upload --- CarCrimeCity/Part1/car_top.png | Bin 0 -> 185747 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 CarCrimeCity/Part1/car_top.png diff --git a/CarCrimeCity/Part1/car_top.png b/CarCrimeCity/Part1/car_top.png new file mode 100644 index 0000000000000000000000000000000000000000..ad89ae40704b163543421d131d3888426177ff1c GIT binary patch literal 185747 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

p7QxR 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

o6qv2 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�q*0q=y7RI#N2E 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%0Dx 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%0A}gb)aXjx`DvSoUT3XhdV8nkcxm6)1H9b$ aIWb_J>nytHA@$#xYzhxl?ib!Oe*J$lKX3#9 literal 0 HcmV?d00001 From 1c12d5b946e1d5787aaa57c0b8b3a4abd9a5b1e5 Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sat, 2 Feb 2019 17:07:45 +0000 Subject: [PATCH 08/12] Added Polygon Collisions #1 --- OneLoneCoder_PGE_PolygonCollisions1.cpp | 434 ++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 OneLoneCoder_PGE_PolygonCollisions1.cpp diff --git a/OneLoneCoder_PGE_PolygonCollisions1.cpp b/OneLoneCoder_PGE_PolygonCollisions1.cpp new file mode 100644 index 0000000..9735fe5 --- /dev/null +++ b/OneLoneCoder_PGE_PolygonCollisions1.cpp @@ -0,0 +1,434 @@ +/* + Convex Polygon Collision Detection + "Don't you dare try concave ones..." - javidx9 + + License (OLC-3) + ~~~~~~~~~~~~~~~ + + Copyright 2018-2019 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. + + Instructions: + ~~~~~~~~~~~~~ + Use arrow keys to control pentagon + Use WASD to control triangle + F1..F4 selects algorithm + + Relevant Video: https://youtu.be/7Ik2vowGcU0 + + 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 + Patreon: https://www.patreon.com/javidx9 + Homepage: https://www.onelonecoder.com + + Author + ~~~~~~ + David Barr, aka javidx9, ©OneLoneCoder 2019 +*/ + +#define OLC_PGE_APPLICATION +#include "olcPixelGameEngine.h" + +#include +#include + +// Override base class with your custom functionality +class PolygonCollisions : public olc::PixelGameEngine +{ +public: + PolygonCollisions() + { + sAppName = "Polygon Collisions"; + } + + struct vec2d + { + float x; + float y; + }; + + struct polygon + { + std::vector p; // Transformed Points + vec2d pos; // Position of shape + float angle; // Direction of shape + std::vector o; // "Model" of shape + bool overlap = false; // Flag to indicate if overlap has occurred + }; + + std::vector vecShapes; + + int nMode = 0; + +public: + bool OnUserCreate() override + { + // Create Pentagon + polygon s1; + float fTheta = 3.14159f * 2.0f / 5.0f; + s1.pos = { 100, 100 }; + s1.angle = 0.0f; + for (int i = 0; i < 5; i++) + { + s1.o.push_back({ 30.0f * cosf(fTheta * i), 30.0f * sinf(fTheta * i) }); + s1.p.push_back({ 30.0f * cosf(fTheta * i), 30.0f * sinf(fTheta * i) }); + } + + // Create Triangle + polygon s2; + fTheta = 3.14159f * 2.0f / 3.0f; + s2.pos = { 200, 150 }; + s2.angle = 0.0f; + for (int i = 0; i < 3; i++) + { + s2.o.push_back({ 20.0f * cosf(fTheta * i), 20.0f * sinf(fTheta * i) }); + s2.p.push_back({ 20.0f * cosf(fTheta * i), 20.0f * sinf(fTheta * i) }); + } + + // Create Quad + polygon s3; + s3.pos = { 50, 200 }; + s3.angle = 0.0f; + s3.o.push_back({ -30, -30 }); + s3.o.push_back({ -30, +30 }); + s3.o.push_back({ +30, +30 }); + s3.o.push_back({ +30, -30 }); + s3.p.resize(4); + + + vecShapes.push_back(s1); + vecShapes.push_back(s2); + vecShapes.push_back(s3); + return true; + } + + + + bool ShapeOverlap_SAT(polygon &r1, polygon &r2) + { + polygon *poly1 = &r1; + polygon *poly2 = &r2; + + for (int shape = 0; shape < 2; shape++) + { + if (shape == 1) + { + poly1 = &r2; + poly2 = &r1; + } + + for (int a = 0; a < poly1->p.size(); a++) + { + int b = (a + 1) % poly1->p.size(); + vec2d axisProj = { -(poly1->p[b].y - poly1->p[a].y), poly1->p[b].x - poly1->p[a].x }; + float d = sqrtf(axisProj.x * axisProj.x + axisProj.y * axisProj.y); + axisProj = { axisProj.x / d, axisProj.y / d }; + + // Work out min and max 1D points for r1 + float min_r1 = INFINITY, max_r1 = -INFINITY; + for (int p = 0; p < poly1->p.size(); p++) + { + float q = (poly1->p[p].x * axisProj.x + poly1->p[p].y * axisProj.y); + min_r1 = std::min(min_r1, q); + max_r1 = std::max(max_r1, q); + } + + // Work out min and max 1D points for r2 + float min_r2 = INFINITY, max_r2 = -INFINITY; + for (int p = 0; p < poly2->p.size(); p++) + { + float q = (poly2->p[p].x * axisProj.x + poly2->p[p].y * axisProj.y); + min_r2 = std::min(min_r2, q); + max_r2 = std::max(max_r2, q); + } + + if (!(max_r2 >= min_r1 && max_r1 >= min_r2)) + return false; + } + } + + return true; + } + + bool ShapeOverlap_SAT_STATIC(polygon &r1, polygon &r2) + { + polygon *poly1 = &r1; + polygon *poly2 = &r2; + + float overlap = INFINITY; + + for (int shape = 0; shape < 2; shape++) + { + if (shape == 1) + { + poly1 = &r2; + poly2 = &r1; + } + + for (int a = 0; a < poly1->p.size(); a++) + { + int b = (a + 1) % poly1->p.size(); + vec2d axisProj = { -(poly1->p[b].y - poly1->p[a].y), poly1->p[b].x - poly1->p[a].x }; + + // Optional normalisation of projection axis enhances stability slightly + //float d = sqrtf(axisProj.x * axisProj.x + axisProj.y * axisProj.y); + //axisProj = { axisProj.x / d, axisProj.y / d }; + + // Work out min and max 1D points for r1 + float min_r1 = INFINITY, max_r1 = -INFINITY; + for (int p = 0; p < poly1->p.size(); p++) + { + float q = (poly1->p[p].x * axisProj.x + poly1->p[p].y * axisProj.y); + min_r1 = std::min(min_r1, q); + max_r1 = std::max(max_r1, q); + } + + // Work out min and max 1D points for r2 + float min_r2 = INFINITY, max_r2 = -INFINITY; + for (int p = 0; p < poly2->p.size(); p++) + { + float q = (poly2->p[p].x * axisProj.x + poly2->p[p].y * axisProj.y); + min_r2 = std::min(min_r2, q); + max_r2 = std::max(max_r2, q); + } + + // Calculate actual overlap along projected axis, and store the minimum + overlap = std::min(std::min(max_r1, max_r2) - std::max(min_r1, min_r2), overlap); + + if (!(max_r2 >= min_r1 && max_r1 >= min_r2)) + return false; + } + } + + // If we got here, the objects have collided, we will displace r1 + // by overlap along the vector between the two object centers + vec2d d = { r2.pos.x - r1.pos.x, r2.pos.y - r1.pos.y }; + float s = sqrtf(d.x*d.x + d.y*d.y); + r1.pos.x -= overlap * d.x / s; + r1.pos.y -= overlap * d.y / s; + return false; + } + + // Use edge/diagonal intersections. + bool ShapeOverlap_DIAGS(polygon &r1, polygon &r2) + { + polygon *poly1 = &r1; + polygon *poly2 = &r2; + + for (int shape = 0; shape < 2; shape++) + { + if (shape == 1) + { + poly1 = &r2; + poly2 = &r1; + } + + // Check diagonals of polygon... + for (int p = 0; p < poly1->p.size(); p++) + { + vec2d line_r1s = poly1->pos; + vec2d line_r1e = poly1->p[p]; + + // ...against edges of the other + for (int q = 0; q < poly2->p.size(); q++) + { + vec2d line_r2s = poly2->p[q]; + vec2d line_r2e = poly2->p[(q + 1) % poly2->p.size()]; + + // Standard "off the shelf" line segment intersection + float h = (line_r2e.x - line_r2s.x) * (line_r1s.y - line_r1e.y) - (line_r1s.x - line_r1e.x) * (line_r2e.y - line_r2s.y); + float t1 = ((line_r2s.y - line_r2e.y) * (line_r1s.x - line_r2s.x) + (line_r2e.x - line_r2s.x) * (line_r1s.y - line_r2s.y)) / h; + float t2 = ((line_r1s.y - line_r1e.y) * (line_r1s.x - line_r2s.x) + (line_r1e.x - line_r1s.x) * (line_r1s.y - line_r2s.y)) / h; + + if (t1 >= 0.0f && t1 < 1.0f && t2 >= 0.0f && t2 < 1.0f) + { + return true; + } + } + } + } + return false; + } + + // Use edge/diagonal intersections. + bool ShapeOverlap_DIAGS_STATIC(polygon &r1, polygon &r2) + { + polygon *poly1 = &r1; + polygon *poly2 = &r2; + + for (int shape = 0; shape < 2; shape++) + { + if (shape == 1) + { + poly1 = &r2; + poly2 = &r1; + } + + // Check diagonals of this polygon... + for (int p = 0; p < poly1->p.size(); p++) + { + vec2d line_r1s = poly1->pos; + vec2d line_r1e = poly1->p[p]; + + vec2d displacement = { 0,0 }; + + // ...against edges of this polygon + for (int q = 0; q < poly2->p.size(); q++) + { + vec2d line_r2s = poly2->p[q]; + vec2d line_r2e = poly2->p[(q + 1) % poly2->p.size()]; + + // Standard "off the shelf" line segment intersection + float h = (line_r2e.x - line_r2s.x) * (line_r1s.y - line_r1e.y) - (line_r1s.x - line_r1e.x) * (line_r2e.y - line_r2s.y); + float t1 = ((line_r2s.y - line_r2e.y) * (line_r1s.x - line_r2s.x) + (line_r2e.x - line_r2s.x) * (line_r1s.y - line_r2s.y)) / h; + float t2 = ((line_r1s.y - line_r1e.y) * (line_r1s.x - line_r2s.x) + (line_r1e.x - line_r1s.x) * (line_r1s.y - line_r2s.y)) / h; + + if (t1 >= 0.0f && t1 < 1.0f && t2 >= 0.0f && t2 < 1.0f) + { + displacement.x += (1.0f - t1) * (line_r1e.x - line_r1s.x); + displacement.y += (1.0f - t1) * (line_r1e.y - line_r1s.y); + } + } + + r1.pos.x += displacement.x * (shape == 0 ? -1 : +1); + r1.pos.y += displacement.y * (shape == 0 ? -1 : +1); + } + } + + // Cant overlap if static collision is resolved + return false; + } + + + + bool OnUserUpdate(float fElapsedTime) override + { + if (GetKey(olc::Key::F1).bReleased) nMode = 0; + if (GetKey(olc::Key::F2).bReleased) nMode = 1; + if (GetKey(olc::Key::F3).bReleased) nMode = 2; + if (GetKey(olc::Key::F4).bReleased) nMode = 3; + + // Shape 1 + if (GetKey(olc::Key::LEFT).bHeld) vecShapes[0].angle -= 2.0f * fElapsedTime; + if (GetKey(olc::Key::RIGHT).bHeld) vecShapes[0].angle += 2.0f * fElapsedTime; + + if (GetKey(olc::Key::UP).bHeld) + { + vecShapes[0].pos.x += cosf(vecShapes[0].angle) * 60.0f * fElapsedTime; + vecShapes[0].pos.y += sinf(vecShapes[0].angle) * 60.0f * fElapsedTime; + } + + if (GetKey(olc::Key::DOWN).bHeld) + { + vecShapes[0].pos.x -= cosf(vecShapes[0].angle) * 60.0f * fElapsedTime; + vecShapes[0].pos.y -= sinf(vecShapes[0].angle) * 60.0f * fElapsedTime; + } + + // Shape 2 + if (GetKey(olc::Key::A).bHeld) vecShapes[1].angle -= 2.0f * fElapsedTime; + if (GetKey(olc::Key::D).bHeld) vecShapes[1].angle += 2.0f * fElapsedTime; + + if (GetKey(olc::Key::W).bHeld) + { + vecShapes[1].pos.x += cosf(vecShapes[1].angle) * 60.0f * fElapsedTime; + vecShapes[1].pos.y += sinf(vecShapes[1].angle) * 60.0f * fElapsedTime; + } + + if (GetKey(olc::Key::S).bHeld) + { + vecShapes[1].pos.x -= cosf(vecShapes[1].angle) * 60.0f * fElapsedTime; + vecShapes[1].pos.y -= sinf(vecShapes[1].angle) * 60.0f * fElapsedTime; + } + + // Update Shapes and reset flags + for (auto &r : vecShapes) + { + for (int i = 0; i < r.o.size(); i++) + r.p[i] = + { // 2D Rotation Transform + 2D Translation + (r.o[i].x * cosf(r.angle)) - (r.o[i].y * sinf(r.angle)) + r.pos.x, + (r.o[i].x * sinf(r.angle)) + (r.o[i].y * cosf(r.angle)) + r.pos.y, + }; + + r.overlap = false; + } + + // Check for overlap + for (int m = 0; m < vecShapes.size(); m++) + for (int n = m + 1; n < vecShapes.size(); n++) + { + switch (nMode) + { + case 0: vecShapes[m].overlap |= ShapeOverlap_SAT(vecShapes[m], vecShapes[n]); break; + case 1: vecShapes[m].overlap |= ShapeOverlap_SAT_STATIC(vecShapes[m], vecShapes[n]); break; + case 2: vecShapes[m].overlap |= ShapeOverlap_DIAGS(vecShapes[m], vecShapes[n]); break; + case 3: vecShapes[m].overlap |= ShapeOverlap_DIAGS_STATIC(vecShapes[m], vecShapes[n]); break; + } + } + + // === Render Display === + Clear(olc::BLUE); + + // Draw Shapes + for (auto &r : vecShapes) + { + // Draw Boundary + for (int i = 0; i < r.p.size(); i++) + DrawLine(r.p[i].x, r.p[i].y, r.p[(i + 1) % r.p.size()].x, r.p[(i + 1) % r.p.size()].y, (r.overlap ? olc::RED : olc::WHITE)); + + // Draw Direction + DrawLine(r.p[0].x, r.p[0].y, r.pos.x, r.pos.y, (r.overlap ? olc::RED : olc::WHITE)); + } + + // Draw HUD + DrawString(8, 10, "F1: SAT", (nMode == 0 ? olc::RED : olc::YELLOW)); + DrawString(8, 20, "F2: SAT/STATIC", (nMode == 1 ? olc::RED : olc::YELLOW)); + DrawString(8, 30, "F3: DIAG", (nMode == 2 ? olc::RED : olc::YELLOW)); + DrawString(8, 40, "F4: DIAG/STATIC", (nMode == 3 ? olc::RED : olc::YELLOW)); + + return true; + } +}; + + + +int main() +{ + PolygonCollisions demo; + if (demo.Construct(256, 240, 4, 4)) + demo.Start(); + return 0; +} \ No newline at end of file From 2fa06b8f4e3c50a99ab9bee64c6353da78e0b76f Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sun, 3 Feb 2019 13:51:44 +0000 Subject: [PATCH 09/12] Sound 0.3 --- olcPGEX_Sound.h | 793 ++++++++++++++++++++++++++++++------------------ 1 file changed, 504 insertions(+), 289 deletions(-) diff --git a/olcPGEX_Sound.h b/olcPGEX_Sound.h index c6f3b44..0b4dcf3 100644 --- a/olcPGEX_Sound.h +++ b/olcPGEX_Sound.h @@ -3,7 +3,7 @@ +-------------------------------------------------------------+ | OneLoneCoder Pixel Game Engine Extension | - | Sound - v0.2 | + | Sound - v0.3 | +-------------------------------------------------------------+ What is this? @@ -11,10 +11,18 @@ This is an extension to the olcPixelGameEngine, which provides sound generation and wave playing routines. + Special Thanks: + ~~~~~~~~~~~~~~~ + Slavka - For entire non-windows system back end! + Gorbit99 - Testing, Bug Fixes + Cyberdroid - Testing, Bug Fixes + Dragoneye - Testing + Puol - Testing + License (OLC-3) ~~~~~~~~~~~~~~~ - Copyright 2018 OneLoneCoder.com + Copyright 2018 - 2019 OneLoneCoder.com Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions @@ -56,28 +64,59 @@ Author ~~~~~~ - David Barr, aka javidx9, ©OneLoneCoder 2018 + David Barr, aka javidx9, ©OneLoneCoder 2019 */ -#ifndef OLC_PGEX_SOUND -#define OLC_PGEX_SOUND +#ifndef OLC_PGEX_SOUND_H +#define OLC_PGEX_SOUND_H #include +#include +#include #include #undef min #undef max +// Choose a default sound backend +#if !defined(USE_ALSA) && !defined(USE_OPENAL) && !defined(USE_WINDOWS) +#ifdef __linux__ +#define USE_ALSA +#endif + +#ifdef __EMSCRIPTEN__ +#define USE_OPENAL +#endif + +#ifdef _WIN32 +#define USE_WINDOWS +#endif + +#endif + +#ifdef USE_ALSA +#define ALSA_PCM_NEW_HW_PARAMS_API +#include +#endif + +#ifdef USE_OPENAL +#include +#include +#include +#endif + +#pragma pack(push, 1) typedef struct { - unsigned short wFormatTag; - unsigned short nChannels; - unsigned long nSamplesPerSec; - unsigned long nAvgBytesPerSec; - unsigned short nBlockAlign; - unsigned short wBitsPerSample; - unsigned short cbSize; + uint16_t wFormatTag; + uint16_t nChannels; + uint32_t nSamplesPerSec; + uint32_t nAvgBytesPerSec; + uint16_t nBlockAlign; + uint16_t wBitsPerSample; + uint16_t cbSize; } OLC_WAVEFORMATEX; +#pragma pack(pop) namespace olc { @@ -87,18 +126,18 @@ namespace olc // A representation of an affine transform, used to rotate, scale, offset & shear space public: class AudioSample - { + { public: AudioSample(); AudioSample(std::string sWavFile, olc::ResourcePack *pack = nullptr); olc::rcode LoadFromFile(std::string sWavFile, olc::ResourcePack *pack = nullptr); - + public: OLC_WAVEFORMATEX wavHeader; float *fSample = nullptr; long nSamples = 0; int nChannels = 0; - bool bSampleValid = false; + bool bSampleValid = false; }; struct sCurrentlyPlayingSample @@ -119,7 +158,7 @@ namespace olc static void SetUserFilterFunction(std::function func); public: - static unsigned int LoadAudioSample(std::string sWavFile, olc::ResourcePack *pack = nullptr); + static int LoadAudioSample(std::string sWavFile, olc::ResourcePack *pack = nullptr); static void PlaySample(int id, bool bLoop = false); static void StopSample(int id); static void StopAll(); @@ -127,8 +166,8 @@ namespace olc private: -#ifdef WIN32 // Windows specific sound management - static void CALLBACK waveOutProc(HWAVEOUT hWaveOut, UINT uMsg, DWORD dwParam1, DWORD dwParam2); +#ifdef USE_WINDOWS // Windows specific sound management + static void CALLBACK waveOutProc(HWAVEOUT hWaveOut, UINT uMsg, DWORD dwParam1, DWORD dwParam2); static unsigned int m_nSampleRate; static unsigned int m_nChannels; static unsigned int m_nBlockCount; @@ -136,26 +175,47 @@ namespace olc static unsigned int m_nBlockCurrent; static short* m_pBlockMemory; static WAVEHDR *m_pWaveHeaders; - static HWAVEOUT m_hwDevice; + static HWAVEOUT m_hwDevice; static std::atomic m_nBlockFree; static std::condition_variable m_cvBlockNotZero; static std::mutex m_muxBlockNotZero; #endif +#ifdef USE_ALSA + static snd_pcm_t *m_pPCM; + static unsigned int m_nSampleRate; + static unsigned int m_nChannels; + static unsigned int m_nBlockSamples; + static short* m_pBlockMemory; +#endif + +#ifdef USE_OPENAL + static std::queue m_qAvailableBuffers; + static ALuint *m_pBuffers; + static ALuint m_nSource; + static ALCdevice *m_pDevice; + static ALCcontext *m_pContext; + static unsigned int m_nSampleRate; + static unsigned int m_nChannels; + static unsigned int m_nBlockCount; + static unsigned int m_nBlockSamples; + static short* m_pBlockMemory; +#endif + static void AudioThread(); static std::thread m_AudioThread; static std::atomic m_bAudioThreadActive; static std::atomic m_fGlobalTime; static std::function funcUserSynth; static std::function funcUserFilter; - - }; } -#ifdef WIN32 -#pragma comment(lib, "winmm.lib") +// Implementation, platform-independent + +#ifdef OLC_PGEX_SOUND +#undef OLC_PGEX_SOUND namespace olc { @@ -187,20 +247,20 @@ namespace olc // which are not in the wav file // Just check if wave format is compatible with olcPGE - if (wavHeader.wBitsPerSample != 16 || wavHeader.nSamplesPerSec != 44100) + if (wavHeader.wBitsPerSample != 16 || wavHeader.nSamplesPerSec != 44100) return olc::FAIL; - + // Search for audio data chunk - long nChunksize = 0; + uint32_t nChunksize = 0; is.read(dump, sizeof(char) * 4); // Read chunk header - is.read((char*)&nChunksize, sizeof(long)); // Read chunk size + is.read((char*)&nChunksize, sizeof(uint32_t)); // Read chunk size while (strncmp(dump, "data", 4) != 0) { // Not audio data, so just skip it //std::fseek(f, nChunksize, SEEK_CUR); is.seekg(nChunksize, std::istream::cur); is.read(dump, sizeof(char) * 4); - is.read((char*)&nChunksize, sizeof(long)); + is.read((char*)&nChunksize, sizeof(uint32_t)); } // Finally got to data, so read it all in and convert to float samples @@ -221,20 +281,21 @@ namespace olc { is.read((char*)&s, sizeof(short)); - *pSample = (float)s / (float)(MAXSHORT); + *pSample = (float)s / (float)(SHRT_MAX); pSample++; } } } - // All done, flag sound as valid + // All done, flag sound as valid bSampleValid = true; return olc::OK; }; - + if (pack != nullptr) { - std::istream is(&(pack->GetStreamBuffer(sWavFile))); + olc::ResourcePack::sEntry entry = pack->GetStreamBuffer(sWavFile); + std::istream is(&entry); return ReadWave(is); } else @@ -250,6 +311,131 @@ namespace olc } } + // This vector holds all loaded sound samples in memory + std::vector vecAudioSamples; + + // This structure represents a sound that is currently playing. It only + // holds the sound ID and where this instance of it is up to for its + // current playback + + void SOUND::SetUserSynthFunction(std::function func) + { + funcUserSynth = func; + } + + void SOUND::SetUserFilterFunction(std::function func) + { + funcUserFilter = func; + } + + // Load a 16-bit WAVE file @ 44100Hz ONLY into memory. A sample ID + // number is returned if successful, otherwise -1 + int SOUND::LoadAudioSample(std::string sWavFile, olc::ResourcePack *pack) + { + + olc::SOUND::AudioSample a(sWavFile, pack); + if (a.bSampleValid) + { + vecAudioSamples.push_back(a); + return (unsigned int)vecAudioSamples.size(); + } + else + return -1; + } + + // Add sample 'id' to the mixers sounds to play list + void SOUND::PlaySample(int id, bool bLoop) + { + olc::SOUND::sCurrentlyPlayingSample a; + a.nAudioSampleID = id; + a.nSamplePosition = 0; + a.bFinished = false; + a.bFlagForStop = false; + a.bLoop = bLoop; + SOUND::listActiveSamples.push_back(a); + } + + void SOUND::StopSample(int id) + { + // Find first occurence of sample id + auto s = std::find_if(listActiveSamples.begin(), listActiveSamples.end(), [&](const olc::SOUND::sCurrentlyPlayingSample &s) { return s.nAudioSampleID == id; }); + if (s != listActiveSamples.end()) + s->bFlagForStop = true; + } + + void SOUND::StopAll() + { + for (auto &s : listActiveSamples) + { + s.bFlagForStop = true; + } + } + + float SOUND::GetMixerOutput(int nChannel, float fGlobalTime, float fTimeStep) + { + // Accumulate sample for this channel + float fMixerSample = 0.0f; + + for (auto &s : listActiveSamples) + { + if (m_bAudioThreadActive) + { + if (s.bFlagForStop) + { + s.bLoop = false; + s.bFinished = true; + } + else + { + // Calculate sample position + s.nSamplePosition += roundf((float)vecAudioSamples[s.nAudioSampleID - 1].wavHeader.nSamplesPerSec * fTimeStep); + + // If sample position is valid add to the mix + if (s.nSamplePosition < vecAudioSamples[s.nAudioSampleID - 1].nSamples) + fMixerSample += vecAudioSamples[s.nAudioSampleID - 1].fSample[(s.nSamplePosition * vecAudioSamples[s.nAudioSampleID - 1].nChannels) + nChannel]; + else + { + if (s.bLoop) + { + s.nSamplePosition = 0; + } + else + s.bFinished = true; // Else sound has completed + } + } + } + else + return 0.0f; + } + + // If sounds have completed then remove them + listActiveSamples.remove_if([](const sCurrentlyPlayingSample &s) {return s.bFinished; }); + + // The users application might be generating sound, so grab that if it exists + if (funcUserSynth != nullptr) + fMixerSample += funcUserSynth(nChannel, fGlobalTime, fTimeStep); + + // Return the sample via an optional user override to filter the sound + if (funcUserFilter != nullptr) + return funcUserFilter(nChannel, fGlobalTime, fMixerSample); + else + return fMixerSample; + } + + std::thread SOUND::m_AudioThread; + std::atomic SOUND::m_bAudioThreadActive{ false }; + std::atomic SOUND::m_fGlobalTime{ 0.0f }; + std::list SOUND::listActiveSamples; + std::function SOUND::funcUserSynth = nullptr; + std::function SOUND::funcUserFilter = nullptr; +} + +// Implementation, Windows-specific +#ifdef USE_WINDOWS +#pragma comment(lib, "winmm.lib") + +namespace olc +{ bool SOUND::InitialiseAudio(unsigned int nSampleRate, unsigned int nChannels, unsigned int nBlocks, unsigned int nBlockSamples) { // Initialise Sound Engine @@ -386,118 +572,6 @@ namespace olc } } - // This vector holds all loaded sound samples in memory - std::vector vecAudioSamples; - - // This structure represents a sound that is currently playing. It only - // holds the sound ID and where this instance of it is up to for its - // current playback - - void SOUND::SetUserSynthFunction(std::function func) - { - funcUserSynth = func; - } - - void SOUND::SetUserFilterFunction(std::function func) - { - funcUserFilter = func; - } - - // Load a 16-bit WAVE file @ 44100Hz ONLY into memory. A sample ID - // number is returned if successful, otherwise -1 - unsigned int SOUND::LoadAudioSample(std::string sWavFile, olc::ResourcePack *pack) - { - - olc::SOUND::AudioSample a(sWavFile, pack); - if (a.bSampleValid) - { - vecAudioSamples.push_back(a); - return vecAudioSamples.size(); - } - else - return -1; - } - - // Add sample 'id' to the mixers sounds to play list - void SOUND::PlaySample(int id, bool bLoop) - { - olc::SOUND::sCurrentlyPlayingSample a; - a.nAudioSampleID = id; - a.nSamplePosition = 0; - a.bFinished = false; - a.bFlagForStop = false; - a.bLoop = bLoop; - SOUND::listActiveSamples.push_back(a); - } - - void SOUND::StopSample(int id) - { - // Find first occurence of sample id - auto s = std::find_if(listActiveSamples.begin(), listActiveSamples.end(), [&](const olc::SOUND::sCurrentlyPlayingSample &s) { return s.nAudioSampleID == id; }); - if(s != listActiveSamples.end()) - s->bFlagForStop = true; - } - - void SOUND::StopAll() - { - for (auto &s : listActiveSamples) - { - s.bFlagForStop = true; - } - } - - float SOUND::GetMixerOutput(int nChannel, float fGlobalTime, float fTimeStep) - { - // Accumulate sample for this channel - float fMixerSample = 0.0f; - - for (auto &s : listActiveSamples) - { - if (m_bAudioThreadActive) - { - if (s.bFlagForStop) - { - s.bLoop = false; - s.bFinished = true; - } - else - { - // Calculate sample position - s.nSamplePosition += (long)((float)vecAudioSamples[s.nAudioSampleID - 1].wavHeader.nSamplesPerSec * fTimeStep); - - // If sample position is valid add to the mix - if (s.nSamplePosition < vecAudioSamples[s.nAudioSampleID - 1].nSamples) - fMixerSample += vecAudioSamples[s.nAudioSampleID - 1].fSample[(s.nSamplePosition * vecAudioSamples[s.nAudioSampleID - 1].nChannels) + nChannel]; - else - { - if (s.bLoop) - { - s.nSamplePosition = 0; - } - else - s.bFinished = true; // Else sound has completed - } - } - } - else - return 0.0f; - } - - // If sounds have completed then remove them - listActiveSamples.remove_if([](const sCurrentlyPlayingSample &s) {return s.bFinished; }); - - // The users application might be generating sound, so grab that if it exists - if(funcUserSynth != nullptr) - fMixerSample += funcUserSynth(nChannel, fGlobalTime, fTimeStep); - - // Return the sample via an optional user override to filter the sound - if (funcUserFilter != nullptr) - return funcUserFilter(nChannel, fGlobalTime, fMixerSample); - else - return fMixerSample; - } - - unsigned int SOUND::m_nSampleRate = 0; unsigned int SOUND::m_nChannels = 0; unsigned int SOUND::m_nBlockCount = 0; @@ -506,35 +580,295 @@ namespace olc short* SOUND::m_pBlockMemory = nullptr; WAVEHDR *SOUND::m_pWaveHeaders = nullptr; HWAVEOUT SOUND::m_hwDevice; - std::thread SOUND::m_AudioThread; - std::atomic SOUND::m_bAudioThreadActive = false; std::atomic SOUND::m_nBlockFree = 0; std::condition_variable SOUND::m_cvBlockNotZero; std::mutex SOUND::m_muxBlockNotZero; - std::atomic SOUND::m_fGlobalTime = 0.0f; - std::list SOUND::listActiveSamples; - std::function SOUND::funcUserSynth = nullptr; - std::function SOUND::funcUserFilter = nullptr; } -#else // Non Windows +#elif defined(USE_ALSA) + namespace olc { - SOUND::AudioSample::AudioSample() - {} - - SOUND::AudioSample::AudioSample(std::string sWavFile, olc::ResourcePack *pack) - { - LoadFromFile(sWavFile, pack); - } - - olc::rcode SOUND::AudioSample::LoadFromFile(std::string sWavFile, olc::ResourcePack *pack) - { - return olc::OK; - } - bool SOUND::InitialiseAudio(unsigned int nSampleRate, unsigned int nChannels, unsigned int nBlocks, unsigned int nBlockSamples) - { + { + // Initialise Sound Engine + m_bAudioThreadActive = false; + m_nSampleRate = nSampleRate; + m_nChannels = nChannels; + m_nBlockSamples = nBlockSamples; + m_pBlockMemory = nullptr; + + // Open PCM stream + int rc = snd_pcm_open(&m_pPCM, "default", SND_PCM_STREAM_PLAYBACK, 0); + if (rc < 0) + return DestroyAudio(); + + + // Prepare the parameter structure and set default parameters + snd_pcm_hw_params_t *params; + snd_pcm_hw_params_alloca(¶ms); + snd_pcm_hw_params_any(m_pPCM, params); + + // Set other parameters + snd_pcm_hw_params_set_format(m_pPCM, params, SND_PCM_FORMAT_S16_LE); + snd_pcm_hw_params_set_rate(m_pPCM, params, m_nSampleRate, 0); + snd_pcm_hw_params_set_channels(m_pPCM, params, m_nChannels); + snd_pcm_hw_params_set_period_size(m_pPCM, params, m_nBlockSamples, 0); + snd_pcm_hw_params_set_periods(m_pPCM, params, nBlocks, 0); + + // Save these parameters + rc = snd_pcm_hw_params(m_pPCM, params); + if (rc < 0) + return DestroyAudio(); + + listActiveSamples.clear(); + + // Allocate Wave|Block Memory + m_pBlockMemory = new short[m_nBlockSamples]; + if (m_pBlockMemory == nullptr) + return DestroyAudio(); + std::fill(m_pBlockMemory, m_pBlockMemory + m_nBlockSamples, 0); + + // Unsure if really needed, helped prevent underrun on my setup + snd_pcm_start(m_pPCM); + for (unsigned int i = 0; i < nBlocks; i++) + rc = snd_pcm_writei(m_pPCM, m_pBlockMemory, 512); + + snd_pcm_start(m_pPCM); + m_bAudioThreadActive = true; + m_AudioThread = std::thread(&SOUND::AudioThread); + + return true; + } + + // Stop and clean up audio system + bool SOUND::DestroyAudio() + { + m_bAudioThreadActive = false; + m_AudioThread.join(); + snd_pcm_drain(m_pPCM); + snd_pcm_close(m_pPCM); + return false; + } + + + // Audio thread. This loop responds to requests from the soundcard to fill 'blocks' + // with audio data. If no requests are available it goes dormant until the sound + // card is ready for more data. The block is fille by the "user" in some manner + // and then issued to the soundcard. + void SOUND::AudioThread() + { + m_fGlobalTime = 0.0f; + static float fTimeStep = 1.0f / (float)m_nSampleRate; + + // Goofy hack to get maximum integer for a type at run-time + short nMaxSample = (short)pow(2, (sizeof(short) * 8) - 1) - 1; + float fMaxSample = (float)nMaxSample; + short nPreviousSample = 0; + + while (m_bAudioThreadActive) + { + short nNewSample = 0; + + auto clip = [](float fSample, float fMax) + { + if (fSample >= 0.0) + return fmin(fSample, fMax); + else + return fmax(fSample, -fMax); + }; + + for (unsigned int n = 0; n < m_nBlockSamples; n += m_nChannels) + { + // User Process + for (unsigned int c = 0; c < m_nChannels; c++) + { + nNewSample = (short)(clip(GetMixerOutput(c, m_fGlobalTime, fTimeStep), 1.0) * fMaxSample); + m_pBlockMemory[n + c] = nNewSample; + nPreviousSample = nNewSample; + } + + m_fGlobalTime = m_fGlobalTime + fTimeStep; + } + + // Send block to sound device + snd_pcm_uframes_t nLeft = m_nBlockSamples; + short *pBlockPos = m_pBlockMemory; + while (nLeft > 0) + { + int rc = snd_pcm_writei(m_pPCM, pBlockPos, nLeft); + if (rc > 0) + { + pBlockPos += rc * m_nChannels; + nLeft -= rc; + } + if (rc == -EAGAIN) continue; + if (rc == -EPIPE) // an underrun occured, prepare the device for more data + snd_pcm_prepare(m_pPCM); + } + } + } + + snd_pcm_t* SOUND::m_pPCM = nullptr; + unsigned int SOUND::m_nSampleRate = 0; + unsigned int SOUND::m_nChannels = 0; + unsigned int SOUND::m_nBlockSamples = 0; + short* SOUND::m_pBlockMemory = nullptr; +} + +#elif defined(USE_OPENAL) + +namespace olc +{ + bool SOUND::InitialiseAudio(unsigned int nSampleRate, unsigned int nChannels, unsigned int nBlocks, unsigned int nBlockSamples) + { + // Initialise Sound Engine + m_bAudioThreadActive = false; + m_nSampleRate = nSampleRate; + m_nChannels = nChannels; + m_nBlockCount = nBlocks; + m_nBlockSamples = nBlockSamples; + m_pBlockMemory = nullptr; + + // Open the device and create the context + m_pDevice = alcOpenDevice(NULL); + if (m_pDevice) + { + m_pContext = alcCreateContext(m_pDevice, NULL); + alcMakeContextCurrent(m_pContext); + } + else + return DestroyAudio(); + + // Allocate memory for sound data + alGetError(); + m_pBuffers = new ALuint[m_nBlockCount]; + alGenBuffers(m_nBlockCount, m_pBuffers); + alGenSources(1, &m_nSource); + + for (unsigned int i = 0; i < m_nBlockCount; i++) + m_qAvailableBuffers.push(m_pBuffers[i]); + + listActiveSamples.clear(); + + // Allocate Wave|Block Memory + m_pBlockMemory = new short[m_nBlockSamples]; + if (m_pBlockMemory == nullptr) + return DestroyAudio(); + std::fill(m_pBlockMemory, m_pBlockMemory + m_nBlockSamples, 0); + + m_bAudioThreadActive = true; + m_AudioThread = std::thread(&SOUND::AudioThread); + return true; + } + + // Stop and clean up audio system + bool SOUND::DestroyAudio() + { + m_bAudioThreadActive = false; + m_AudioThread.join(); + + alDeleteBuffers(m_nBlockCount, m_pBuffers); + delete[] m_pBuffers; + alDeleteSources(1, &m_nSource); + + alcMakeContextCurrent(NULL); + alcDestroyContext(m_pContext); + alcCloseDevice(m_pDevice); + return false; + } + + + // Audio thread. This loop responds to requests from the soundcard to fill 'blocks' + // with audio data. If no requests are available it goes dormant until the sound + // card is ready for more data. The block is fille by the "user" in some manner + // and then issued to the soundcard. + void SOUND::AudioThread() + { + m_fGlobalTime = 0.0f; + static float fTimeStep = 1.0f / (float)m_nSampleRate; + + // Goofy hack to get maximum integer for a type at run-time + short nMaxSample = (short)pow(2, (sizeof(short) * 8) - 1) - 1; + float fMaxSample = (float)nMaxSample; + short nPreviousSample = 0; + + std::vector vProcessed; + + while (m_bAudioThreadActive) + { + ALint nState, nProcessed; + alGetSourcei(m_nSource, AL_SOURCE_STATE, &nState); + alGetSourcei(m_nSource, AL_BUFFERS_PROCESSED, &nProcessed); + + // Add processed buffers to our queue + vProcessed.resize(nProcessed); + alSourceUnqueueBuffers(m_nSource, nProcessed, vProcessed.data()); + for (ALint nBuf : vProcessed) m_qAvailableBuffers.push(nBuf); + + // Wait until there is a free buffer (ewww) + if (m_qAvailableBuffers.empty()) continue; + + short nNewSample = 0; + + auto clip = [](float fSample, float fMax) + { + if (fSample >= 0.0) + return fmin(fSample, fMax); + else + return fmax(fSample, -fMax); + }; + + for (unsigned int n = 0; n < m_nBlockSamples; n += m_nChannels) + { + // User Process + for (unsigned int c = 0; c < m_nChannels; c++) + { + nNewSample = (short)(clip(GetMixerOutput(c, m_fGlobalTime, fTimeStep), 1.0) * fMaxSample); + m_pBlockMemory[n + c] = nNewSample; + nPreviousSample = nNewSample; + } + + m_fGlobalTime = m_fGlobalTime + fTimeStep; + } + + // Fill OpenAL data buffer + alBufferData( + m_qAvailableBuffers.front(), + m_nChannels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, + m_pBlockMemory, + 2 * m_nBlockSamples, + m_nSampleRate + ); + // Add it to the OpenAL queue + alSourceQueueBuffers(m_nSource, 1, &m_qAvailableBuffers.front()); + // Remove it from ours + m_qAvailableBuffers.pop(); + + // If it's not playing for some reason, change that + if (nState != AL_PLAYING) + alSourcePlay(m_nSource); + } + } + + std::queue SOUND::m_qAvailableBuffers; + ALuint *SOUND::m_pBuffers = nullptr; + ALuint SOUND::m_nSource = 0; + ALCdevice *SOUND::m_pDevice = nullptr; + ALCcontext *SOUND::m_pContext = nullptr; + unsigned int SOUND::m_nSampleRate = 0; + unsigned int SOUND::m_nChannels = 0; + unsigned int SOUND::m_nBlockCount = 0; + unsigned int SOUND::m_nBlockSamples = 0; + short* SOUND::m_pBlockMemory = nullptr; +} + +#else // Some other platform + +namespace olc +{ + bool SOUND::InitialiseAudio(unsigned int nSampleRate, unsigned int nChannels, unsigned int nBlocks, unsigned int nBlockSamples) + { return true; } @@ -544,134 +878,15 @@ namespace olc return false; } - + // Audio thread. This loop responds to requests from the soundcard to fill 'blocks' // with audio data. If no requests are available it goes dormant until the sound // card is ready for more data. The block is fille by the "user" in some manner // and then issued to the soundcard. void SOUND::AudioThread() - { - - } - - // This vector holds all loaded sound samples in memory - std::vector vecAudioSamples; - - // This structure represents a sound that is currently playing. It only - // holds the sound ID and where this instance of it is up to for its - // current playback - - void SOUND::SetUserSynthFunction(std::function func) - { - funcUserSynth = func; - } - - void SOUND::SetUserFilterFunction(std::function func) - { - funcUserFilter = func; - } - - // Load a 16-bit WAVE file @ 44100Hz ONLY into memory. A sample ID - // number is returned if successful, otherwise -1 - unsigned int SOUND::LoadAudioSample(std::string sWavFile, olc::ResourcePack *pack) - { - olc::SOUND::AudioSample a(sWavFile, pack); - if (a.bSampleValid) - { - vecAudioSamples.push_back(a); - return vecAudioSamples.size(); - } - else - return -1; - } - - // Add sample 'id' to the mixers sounds to play list - void SOUND::PlaySample(int id, bool bLoop) - { - olc::SOUND::sCurrentlyPlayingSample a; - a.nAudioSampleID = id; - a.nSamplePosition = 0; - a.bFinished = false; - a.bFlagForStop = false; - a.bLoop = bLoop; - SOUND::listActiveSamples.push_back(a); - } - - void SOUND::StopSample(int id) - { - // Find first occurence of sample id - auto s = std::find_if(listActiveSamples.begin(), listActiveSamples.end(), [&](const olc::SOUND::sCurrentlyPlayingSample &s) { return s.nAudioSampleID == id; }); - if (s != listActiveSamples.end()) - s->bFlagForStop = true; - } - - void SOUND::StopAll() - { - for (auto &s : listActiveSamples) - { - s.bFlagForStop = true; - } - } - - float SOUND::GetMixerOutput(int nChannel, float fGlobalTime, float fTimeStep) - { - // Accumulate sample for this channel - float fMixerSample = 0.0f; - - for (auto &s : listActiveSamples) - { - if (m_bAudioThreadActive) - { - if (s.bFlagForStop) - { - s.bLoop = false; - s.bFinished = true; - } - else - { - // Calculate sample position - s.nSamplePosition += (long)((float)vecAudioSamples[s.nAudioSampleID - 1].wavHeader.nSamplesPerSec * fTimeStep); - - // If sample position is valid add to the mix - if (s.nSamplePosition < vecAudioSamples[s.nAudioSampleID - 1].nSamples) - fMixerSample += vecAudioSamples[s.nAudioSampleID - 1].fSample[(s.nSamplePosition * vecAudioSamples[s.nAudioSampleID - 1].nChannels) + nChannel]; - else - { - if (s.bLoop) - { - s.nSamplePosition = 0; - } - else - s.bFinished = true; // Else sound has completed - } - } - } - else - return 0.0f; - } - - // If sounds have completed then remove them - listActiveSamples.remove_if([](const sCurrentlyPlayingSample &s) {return s.bFinished; }); - - // The users application might be generating sound, so grab that if it exists - if (funcUserSynth != nullptr) - fMixerSample += funcUserSynth(nChannel, fGlobalTime, fTimeStep); - - // Return the sample via an optional user override to filter the sound - if (funcUserFilter != nullptr) - return funcUserFilter(nChannel, fGlobalTime, fMixerSample); - else - return fMixerSample; - } - - std::thread SOUND::m_AudioThread; - std::atomic SOUND::m_bAudioThreadActive{ false }; - std::atomic SOUND::m_fGlobalTime{ 0.0f }; - std::list SOUND::listActiveSamples; - std::function SOUND::funcUserSynth = nullptr; - std::function SOUND::funcUserFilter = nullptr; + { } } + #endif - - -#endif \ No newline at end of file +#endif +#endif // OLC_PGEX_SOUND \ No newline at end of file From b854c55cb732447d0a4911b7d9373a5085f391db Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sun, 3 Feb 2019 14:23:05 +0000 Subject: [PATCH 10/12] Add files via upload --- OneLoneCoder_PGE_SoundTest.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OneLoneCoder_PGE_SoundTest.cpp b/OneLoneCoder_PGE_SoundTest.cpp index 106b94d..2b9da94 100644 --- a/OneLoneCoder_PGE_SoundTest.cpp +++ b/OneLoneCoder_PGE_SoundTest.cpp @@ -53,6 +53,8 @@ #define OLC_PGE_APPLICATION #include "olcPixelGameEngine.h" + +#define OLC_PGEX_SOUND #include "olcPGEX_Sound.h" #include @@ -114,7 +116,8 @@ private: bool OnUserCreate() { - olc::SOUND::InitialiseAudio(); + olc::SOUND::InitialiseAudio(44100, 1, 8, 512); + sndSampleA = olc::SOUND::LoadAudioSample("SampleA.wav"); sndSampleB = olc::SOUND::LoadAudioSample("SampleB.wav"); sndSampleC = olc::SOUND::LoadAudioSample("SampleC.wav"); From 9d1b6952bee30d22248ac3ba6ba53d9428825034 Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sun, 3 Feb 2019 14:33:47 +0000 Subject: [PATCH 11/12] Updated Graphics 2D --- OneLoneCoder_PGE_ExtensionTestGFX2D.cpp | 19 ++++++++------ olcPGEX_Graphics2D.h | 33 ++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/OneLoneCoder_PGE_ExtensionTestGFX2D.cpp b/OneLoneCoder_PGE_ExtensionTestGFX2D.cpp index c54237a..7794c7d 100644 --- a/OneLoneCoder_PGE_ExtensionTestGFX2D.cpp +++ b/OneLoneCoder_PGE_ExtensionTestGFX2D.cpp @@ -46,7 +46,7 @@ Author ~~~~~~ - David Barr, aka javidx9, ©OneLoneCoder 2018 + David Barr, aka javidx9, ©OneLoneCoder 2018 */ // Include the olcPixelGameEngine @@ -54,6 +54,7 @@ #include "olcPixelGameEngine.h" // To use an extension, just include it +#define OLC_PGE_GRAPHICS2D #include "olcPGEX_Graphics2D.h" class TestExtension : public olc::PixelGameEngine @@ -70,7 +71,7 @@ public: for (int i = 0; i < 16; i++) listEvents.push_back(""); - spr = new olc::Sprite("logo_long.png"); + spr = new olc::Sprite("new_piskel.png"); return true; } @@ -90,8 +91,8 @@ public: DrawCircle(96, 32, 30); // Circle - float mx = GetMouseX(); - float my = GetMouseY(); + float mx = (float)GetMouseX(); + float my = (float)GetMouseY(); float px1 = mx - 32, px2 = mx - 96; float py1 = my - 32, py2 = my - 32; @@ -101,8 +102,8 @@ public: py1 = 22.0f * (py1 * pr1) + 32.0f; px2 = 22.0f * (px2 * pr2) + 96.0f; py2 = 22.0f * (py2 * pr2) + 32.0f; - FillCircle(px1, py1, 8, olc::CYAN); - FillCircle(px2, py2, 8, olc::CYAN); + FillCircle((int32_t)px1, (int32_t)py1, 8, olc::CYAN); + FillCircle((int32_t)px2, (int32_t)py2, 8, olc::CYAN); DrawLine(10, 70, 54, 70); // Lines DrawLine(54, 70, 70, 54); @@ -136,6 +137,8 @@ public: nLog++; } + std::string notes = "CDEFGAB"; + // Test Text scaling and colours DrawString(0, 360, "Text Scale = 1", olc::WHITE, 1); @@ -168,6 +171,8 @@ public: // Use extension to draw sprite with transform applied olc::GFX2D::DrawSprite(spr, t1); + + DrawSprite((int32_t)mx, (int32_t)my, spr, 4); return true; } @@ -181,4 +186,4 @@ int main() demo.Start(); return 0; -} +} \ No newline at end of file diff --git a/olcPGEX_Graphics2D.h b/olcPGEX_Graphics2D.h index 9802128..261a5c0 100644 --- a/olcPGEX_Graphics2D.h +++ b/olcPGEX_Graphics2D.h @@ -3,7 +3,7 @@ +-------------------------------------------------------------+ | OneLoneCoder Pixel Game Engine Extension | - | Advanced 2D Rendering - v0.3 | + | Advanced 2D Rendering - v0.4 | +-------------------------------------------------------------+ What is this? @@ -15,7 +15,7 @@ License (OLC-3) ~~~~~~~~~~~~~~~ - Copyright 2018 OneLoneCoder.com + Copyright 2018 - 2019 OneLoneCoder.com Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions @@ -56,7 +56,7 @@ Author ~~~~~~ - David Barr, aka javidx9, ©OneLoneCoder 2018 + David Barr, aka javidx9, ©OneLoneCoder 2019 */ /* @@ -99,6 +99,8 @@ namespace olc inline void Scale(float sx, float sy); // Append a shear operation (sx, sy) to this transform inline void Shear(float sx, float sy); + + inline void Perspective(float ox, float oy); // Calculate the Forward Transformation of the coordinate (in_x, in_y) -> (out_x, out_y) inline void Forward(float in_x, float in_y, float &out_x, float &out_y); // Calculate the Inverse Transformation of the coordinate (in_x, in_y) -> (out_x, out_y) @@ -121,7 +123,8 @@ namespace olc } - +#ifdef OLC_PGE_GRAPHICS2D +#undef OLC_PGE_GRAPHICS2D namespace olc { @@ -250,16 +253,37 @@ namespace olc Multiply(); } + void olc::GFX2D::Transform2D::Perspective(float ox, float oy) + { + // Construct Translate Matrix + matrix[2][0][0] = 1.0f; matrix[2][1][0] = 0.0f; matrix[2][2][0] = 0.0f; + matrix[2][0][1] = 0.0f; matrix[2][1][1] = 1.0f; matrix[2][2][1] = 0.0f; + matrix[2][0][2] = ox; matrix[2][1][2] = oy; matrix[2][2][2] = 1.0f; + Multiply(); + } + void olc::GFX2D::Transform2D::Forward(float in_x, float in_y, float &out_x, float &out_y) { out_x = in_x * matrix[nSourceMatrix][0][0] + in_y * matrix[nSourceMatrix][1][0] + matrix[nSourceMatrix][2][0]; out_y = in_x * matrix[nSourceMatrix][0][1] + in_y * matrix[nSourceMatrix][1][1] + matrix[nSourceMatrix][2][1]; + float out_z = in_x * matrix[nSourceMatrix][0][2] + in_y * matrix[nSourceMatrix][1][2] + matrix[nSourceMatrix][2][2]; + if (out_z != 0) + { + out_x /= out_z; + out_y /= out_z; + } } void olc::GFX2D::Transform2D::Backward(float in_x, float in_y, float &out_x, float &out_y) { out_x = in_x * matrix[3][0][0] + in_y * matrix[3][1][0] + matrix[3][2][0]; out_y = in_x * matrix[3][0][1] + in_y * matrix[3][1][1] + matrix[3][2][1]; + float out_z = in_x * matrix[3][0][2] + in_y * matrix[3][1][2] + matrix[3][2][2]; + if (out_z != 0) + { + out_x /= out_z; + out_y /= out_z; + } } void olc::GFX2D::Transform2D::Invert() @@ -285,4 +309,5 @@ namespace olc } } +#endif #endif \ No newline at end of file From ee1b03cc083ad81a0f066c1f09c82ce09e0851b5 Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sun, 3 Feb 2019 14:45:28 +0000 Subject: [PATCH 12/12] 1.13 --- olcPixelGameEngine.h | 54 +++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/olcPixelGameEngine.h b/olcPixelGameEngine.h index aa97394..f68712f 100644 --- a/olcPixelGameEngine.h +++ b/olcPixelGameEngine.h @@ -2,7 +2,7 @@ olcPixelGameEngine.h +-------------------------------------------------------------+ - | OneLoneCoder Pixel Game Engine v1.12 | + | OneLoneCoder Pixel Game Engine v1.13 | | "Like the command prompt console one, but not..." - javidx9 | +-------------------------------------------------------------+ @@ -50,7 +50,7 @@ License (OLC-3) ~~~~~~~~~~~~~~~ - Copyright 2018 OneLoneCoder.com + Copyright 2018 - 2019 OneLoneCoder.com Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions @@ -326,7 +326,9 @@ namespace olc // All OneLoneCoder stuff will now exist in the "olc" namespace void SetSampleMode(olc::Sprite::Mode mode = olc::Sprite::Mode::NORMAL); Pixel GetPixel(int32_t x, int32_t y); void SetPixel(int32_t x, int32_t y, Pixel p); + Pixel Sample(float x, float y); + Pixel SampleBL(float u, float v); Pixel* GetData(); private: @@ -547,9 +549,12 @@ namespace olc // All OneLoneCoder stuff will now exist in the "olc" namespace Class2.cpp - #define OLC_PGE_APPLICATION #include "Class2.h" main.cpp - Includes Class1.h and Class2.h - If all of this is a bit too confusing, you can split this file in two! - Everything below this comment block can go into olcPixelGameEngineOOP.cpp - and everything above it can go into olcPixelGameEngineOOP.h + If all else fails, create a file called "olcPixelGameEngine.cpp" with the following + two lines. Then you can just #include "olcPixelGameEngine.h" as normal without worrying + about defining things. Dont forget to include that cpp file as part of your build! + + #define OLC_PGE_APPLICATION + #include "olcPixelGameEngine.h" */ @@ -584,14 +589,9 @@ namespace olc std::wstring w(buffer); delete[] buffer; return w; +#else + return L"SVN FTW!"; #endif -//#ifdef __MINGW32__ -// wchar_t *buffer = new wchar_t[sImageFile.length() + 1]; -// mbstowcs(buffer, sImageFile.c_str(), sImageFile.length()); -// buffer[sImageFile.length()] = L'\0'; -// wsImageFile = buffer; -// delete[] buffer; -//#else } Sprite::Sprite() @@ -825,11 +825,33 @@ namespace olc Pixel Sprite::Sample(float x, float y) { - int32_t sx = (int32_t)(x * (float)width); - int32_t sy = (int32_t)(y * (float)height); + int32_t sx = (int32_t)((x * (float)width) - 0.5f); + int32_t sy = (int32_t)((y * (float)height) - 0.5f); return GetPixel(sx, sy); } + Pixel Sprite::SampleBL(float u, float v) + { + u = u * width - 0.5f; + v = v * height - 0.5f; + int x = (int)u; + int y = (int)v; + float u_ratio = u - x; + float v_ratio = v - y; + float u_opposite = 1 - u_ratio; + float v_opposite = 1 - v_ratio; + + olc::Pixel p1 = GetPixel(x, y); + olc::Pixel p2 = GetPixel(x+1, y); + olc::Pixel p3 = GetPixel(x, y+1); + olc::Pixel p4 = GetPixel(x+1, y+1); + + return olc::Pixel( + (uint8_t)((p1.r * u_opposite + p2.r * u_ratio) * v_opposite + (p3.r * u_opposite + p4.r * u_ratio) * v_ratio), + (uint8_t)((p1.g * u_opposite + p2.g * u_ratio) * v_opposite + (p3.g * u_opposite + p4.g * u_ratio) * v_ratio), + (uint8_t)((p1.b * u_opposite + p2.b * u_ratio) * v_opposite + (p3.b * u_opposite + p4.b * u_ratio) * v_ratio)); + } + Pixel* Sprite::GetData() { return pColData; } //========================================================== @@ -1956,8 +1978,8 @@ namespace olc case WM_MOUSELEAVE: sge->bHasMouseFocus = false; case WM_SETFOCUS: sge->bHasInputFocus = true; return 0; case WM_KILLFOCUS: sge->bHasInputFocus = false; return 0; - case WM_KEYDOWN: sge->pKeyNewState[mapKeys[wParam]] = true; return 0; - case WM_KEYUP: sge->pKeyNewState[mapKeys[wParam]] = false; return 0; + case WM_KEYDOWN: sge->pKeyNewState[mapKeys[(uint16_t)wParam]] = true; return 0; + case WM_KEYUP: sge->pKeyNewState[mapKeys[(uint16_t)wParam]] = false; return 0; case WM_LBUTTONDOWN:sge->pMouseNewState[0] = true; return 0; case WM_LBUTTONUP: sge->pMouseNewState[0] = false; return 0; case WM_RBUTTONDOWN:sge->pMouseNewState[1] = true; return 0;