From 541e67f1772f6b8b6a67dd24f2eca8b4160c7a41 Mon Sep 17 00:00:00 2001 From: karl2805 Date: Thu, 18 Dec 2025 21:15:33 +0000 Subject: [PATCH 01/11] Texture Loaded to Screen --- OpenGL-Core/src/GLCore/Util/Shader.cpp | 54 ++++++++++++++++ OpenGL-Core/src/GLCore/Util/Shader.h | 2 + OpenGL-Core/src/GLCore/Util/Texture.cpp | 61 ++++++++++++++++++ OpenGL-Core/src/GLCore/Util/Texture.h | 15 +++++ .../bin/Debug-windows-x86_64/Glad/Glad.lib | Bin 427648 -> 448510 bytes .../bin/Debug-windows-x86_64/Glad/Glad.pdb | Bin 110592 -> 118784 bytes OpenGL-Sandbox/Shaders/fragment.frag.glsl | 14 ++++ OpenGL-Sandbox/Shaders/vertex.vert.glsl | 15 +++++ OpenGL-Sandbox/assets/container.jpg | Bin 0 -> 25769 bytes OpenGL-Sandbox/src/SandboxLayer.cpp | 61 +++++++++++++++++- OpenGL-Sandbox/src/SandboxLayer.h | 5 ++ 11 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 OpenGL-Core/src/GLCore/Util/Texture.cpp create mode 100644 OpenGL-Core/src/GLCore/Util/Texture.h create mode 100644 OpenGL-Sandbox/Shaders/fragment.frag.glsl create mode 100644 OpenGL-Sandbox/Shaders/vertex.vert.glsl create mode 100644 OpenGL-Sandbox/assets/container.jpg diff --git a/OpenGL-Core/src/GLCore/Util/Shader.cpp b/OpenGL-Core/src/GLCore/Util/Shader.cpp index 02e60ddf..075400ec 100644 --- a/OpenGL-Core/src/GLCore/Util/Shader.cpp +++ b/OpenGL-Core/src/GLCore/Util/Shader.cpp @@ -107,4 +107,58 @@ namespace GLCore::Utils { m_RendererID = program; } + void Shader::CreateComputeShader(const std::string& filepath) + { + std::string shaderSource = ReadFileAsString(filepath); + + GLuint shaderHandle = glCreateShader(GL_COMPUTE_SHADER); + + const GLchar* source = (const GLchar*)shaderSource.c_str(); + glShaderSource(shaderHandle, 1, &source, 0); + + glCompileShader(shaderHandle); + + GLint isCompiled = 0; + glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &isCompiled); + if (isCompiled == GL_FALSE) + { + GLint maxLength = 0; + glGetShaderiv(shaderHandle, GL_INFO_LOG_LENGTH, &maxLength); + + std::vector infoLog(maxLength); + glGetShaderInfoLog(shaderHandle, maxLength, &maxLength, &infoLog[0]); + + std::cerr << infoLog.data() << std::endl; + + glDeleteShader(shaderHandle); + return; + } + + GLuint program = glCreateProgram(); + glAttachShader(program, shaderHandle); + glLinkProgram(program); + + GLint isLinked = 0; + glGetProgramiv(program, GL_LINK_STATUS, (int*)&isLinked); + if (isLinked == GL_FALSE) + { + GLint maxLength = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength); + + std::vector infoLog(maxLength); + glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]); + + std::cerr << infoLog.data() << std::endl; + + glDeleteProgram(program); + glDeleteShader(shaderHandle); + + return; + } + + glDetachShader(program, shaderHandle); + m_ComputeID = program; + } + + } \ No newline at end of file diff --git a/OpenGL-Core/src/GLCore/Util/Shader.h b/OpenGL-Core/src/GLCore/Util/Shader.h index 6431f71a..ff027a38 100644 --- a/OpenGL-Core/src/GLCore/Util/Shader.h +++ b/OpenGL-Core/src/GLCore/Util/Shader.h @@ -19,8 +19,10 @@ namespace GLCore::Utils { void LoadFromGLSLTextFiles(const std::string& vertexShaderPath, const std::string& fragmentShaderPath); GLuint CompileShader(GLenum type, const std::string& source); + void CreateComputeShader(const std::string& filepath); private: GLuint m_RendererID; + GLuint m_ComputeID; }; } \ No newline at end of file diff --git a/OpenGL-Core/src/GLCore/Util/Texture.cpp b/OpenGL-Core/src/GLCore/Util/Texture.cpp new file mode 100644 index 00000000..adc2dbe1 --- /dev/null +++ b/OpenGL-Core/src/GLCore/Util/Texture.cpp @@ -0,0 +1,61 @@ +#include "glpch.h" +#include "Texture.h" +#include "stb_image.h" + +Texture CreateTexture(int width, int height) +{ + Texture result; + result.Width = width; + result.Height = height; + + glCreateTextures(GL_TEXTURE_2D, 1, &result.TextureID); + + glTextureStorage2D(result.TextureID, 1, GL_RGBA32F, width, height); + + glTextureParameteri(result.TextureID, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(result.TextureID, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glTextureParameteri(result.TextureID, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTextureParameteri(result.TextureID, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + return result; +} + + +Texture LoadTexture(const std::string filepath) +{ + int width, height, channels; + + unsigned char* data = stbi_load(filepath.c_str(), &width, &height, &channels, 0); + + if (!data) + { + std::cerr << "Failed to load texture: " << filepath << "\n"; + return {}; + } + + GLenum format = channels == 4 ? GL_RGBA : + channels == 3 ? GL_RGB : + channels == 1 ? GL_RED : 0; + + Texture result; + result.Width = width; + result.Height = height; + + glCreateTextures(GL_TEXTURE_2D, 1, &result.TextureID); + + glTextureStorage2D(result.TextureID, 1, (format == GL_RGBA ? GL_RGBA8 : GL_RGB8), width, height); + + glTextureSubImage2D(result.TextureID, 0, 0, 0, width, height, format, GL_UNSIGNED_BYTE, data); + + glTextureParameteri(result.TextureID, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTextureParameteri(result.TextureID, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTextureParameteri(result.TextureID, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTextureParameteri(result.TextureID, GL_TEXTURE_WRAP_T, GL_REPEAT); + + glGenerateTextureMipmap(result.TextureID); + stbi_image_free(data); + + return result; +} diff --git a/OpenGL-Core/src/GLCore/Util/Texture.h b/OpenGL-Core/src/GLCore/Util/Texture.h new file mode 100644 index 00000000..83949d23 --- /dev/null +++ b/OpenGL-Core/src/GLCore/Util/Texture.h @@ -0,0 +1,15 @@ +#pragma once +#include + +#include + +struct Texture +{ + GLuint TextureID = 0; + uint32_t Width = 0; + uint32_t Height = 0; +}; + +Texture CreateTexture(int width, int height); +Texture LoadTexture(const std::string filepath); + diff --git a/OpenGL-Core/vendor/Glad/bin/Debug-windows-x86_64/Glad/Glad.lib b/OpenGL-Core/vendor/Glad/bin/Debug-windows-x86_64/Glad/Glad.lib index efb8eedd80966b78cec51e3526e451fefb08f8d6..98464ccf83ba37a3b5b0b8e89b7e60818bec0626 100644 GIT binary patch delta 116350 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 Rb)@?|#a*ku9X6@-{{Sj`Bt`%L delta 99188 zcmeFa3Ak2c|Mz`fL(|$tsT9(nkwPgA3Z+5OXbMH8A~b2ZGsLwil!|0nhA1jR$Zgmm zM5c%e8AHe%${3>e^ZTxIop<(od!PF~p5r1(V{oOWx=-jPSox5mQ#@WwH zW#T8NWp*!nE_~Xp@w?M9N0r?be)raRzv-Dq*)MO6k0F-bd~3#?I4{0vdZu>Np6Mg! zW!#qPipKaRo+cNIb+vD0-Fzp1U-Fth+rU0?k)tWEtn$FJzf_CEK-Me9H7pjc8!jxufqv-y8oqgGC(8B7V6y&C})_obb&;7~bFK60O_h;O?`{UL#nAUV@#?^ly*{tJP>YxWQ?xBCim)W$*4`$rz zhvWNfnb$JkwU1<6lalyNn>M;6<1Sm4tZx9*-dvV(Egp><&1BlBM>DSLW6Am+Vws~J z%eZ>WpOKjG`?`GVO?`7f-ZC0DQc0ne1MPo2%Q&vLBq zDCX-OUuM&eKPKyj^pEeeX-BXV^#)|!(9_~KZQ8^$vu^sh_)nX580%YfUet4Aq?tL}Wo@AMyUX*nUE{XqP(^_1Xb(Lnsx7sv2sEe-5x;w9mADP9p^JX(` zPW-_vroCCgxi9Br-BWYp-RCfE_#Ihy^*`go<}l69W2fa=w_$mFI`fq~>WQp7{mE?n z)D;}a3ANwhzyP>EuThip2 ztZTb68`rU=oWZogE3@teME`48%BLh&J)4aSBywAU$FI#aESGtXEq*Q=uQA&ckZq4W zGPT1~I}v*^i80r*xOpUYkcdg-sy^>$tp9x09Y{ip>y_G5uxFM7QK*lHD2(`Js8ktcE$Ef?U>XSurpG7S85-{K9|~0Qu_mTM{4(e(Jy!Z7qhM% z3GGGy)SiJIpW3TadmHxN)V`eB_pqO*w#+KO-0G{ct{w?3_wdyA!4636d8wU_y)Ly6 zrgkNEO=`bM?H||*FZty*26S}Vyp(mFQ#&NJ=U^{P?aisZ2V0Wb^{M?7yEV1DzU-G< z=jE(xNeFghYYJW*>xz!W~tNn6Y0NRUA*j^;K==Lt! z!bN>HV!$h0$G2qaRhsfj*4@mjHa1!s`f$agj{5Wt@YR*c{`2eeH zn46Kp4Jj;2pkBH1EbE`CeGItyer9+fHE#l1`6o8%As)I!ZPp)2mU`uEYbvZ^Yi?u4 zswvb0w5zs)h9or8{;9Q@+L6$*JExQTrEp3*>C6-+0os>KQ<#%ZnwOe;6G#s4V2_&l-nc7K!j@?v1XXY9} zXJ$bP4*^==b1A$7DDyWd`~m2wR9fd}-wTjw253_b0koWMfVSymK+8EJoir&mGXQPI z&A{V}+1n*4tW4ou;P-V*+L}VqYu+>jnts5f!vU?dKcLI`?9^PCnyZ1POUS$+HIJm` zML?VS9-vMAA~oAmQ|5K$Nv^5tNupt{PHOG0X_neHfL7H7NVY4TbT*(hoST|y2_&cN zy4223^W6(*Sx*2u!fOE?^9=@e(7pn+n#>#ifK&&RXMaGG+5H=WE z6TlUKHf~-D_osQDO3kaO`5=XFfTQzVm)ilIsT%A3S!e-h<2wUd#R-76=WIZ0FQn#5 zK-+vfpt+U;I-4s2&AuMcTwkT|E1(6If73&)6q*Cto-Tk6-Kl2MLhdXQTHi!K3!Rco zZkU?`Xoh(y+>=5{3M*4slfru`e4fG&fG(^ZDO7xmeS3$pil`}#QfQe%#}s;{a5A6+ zaz+a0r!Wi9A({{9R{CdZR;FfsYCcQNw$xO3JH3$sEvH#(4ogj+)C@_@xYSHd&2_1{ zGd0Un^HOTw1GE?4q~PB1rfLd}0G-WtDfCKV2%s&T2xw0)Pt6UfxjQwFr|?QT>BH1) z0kj#}cRf^3p$VXSq&=YPtzQbKr*J_Evr|}L&>@B1DGUa5n8u}WX$m)_a8C+Prm!xBO)2~W=!{11d#IhlK`C?tSXZNL z5*y}5rBjMixE9bMT?}aVmI2zCHGmGmrzw1&!j2Rwe!yH-QQ0Gf#wi?}LT5k+qaUDM z9}4Ke{R7atW&qlZ+W?)C2U7D)3a6OYB~TjerF4h0kn!S zfVQUqXfv)(%^g5;vJG^yQ}`l<9e_%|-8X7`>XmE6<<^{pw)+G?OFsjcv5FO+XZh?w zdsCX{Awa8n4p8O~0By>a)Rg(iZ*C1h$EY!&r}CkIO3~v}GX&7Za9#?t0L^t9pgk!` z;Z;D}^C_UKeg~k;dwuLTtOcO^;3(kt)olET)La2*n-?2cW%e;ZHNoeBR?nCU=$eZGZRDbK(#mwwn}ANo zCP3@@DV>zrnz7pA9jSQ=(D8f=(DQC9pe4Ic z{c@_O&=k;KbOLnBj!Vsm)Lf97t5S1&Y92|=DnN(&eLzR!tJG{uO_|Spo~nTI)CIJK z2LL)3ho+`iY6hleWNI!*&5YFC1Zd;$2DGP7rsnn3e4d)^sj2+ApQ|CDx!M8Ri{5~? zZ~(yhYLrc+$E{G642Z?13F7f0Ugxm0iE`D0G*|;0IebSg`cZ(3Jn3B+d}|d zBmGi1C4olSR7?c4n2S@mGKHIgvlp<%L~X>wDZH4%dw{n1+Z1wNX5+8FW+tMh?4QCB zDf9<)u0|Sc;KIKkope)b?gv!Hyb9H#`e?E&4meE@Cg5Cf~{Pq0Z= z)ts4{I{_WrrvM$v^?+9KZJMY2*Z%&i3uqOG0b0c|sTl@n73Tulo!OSh)-?~%Ie#pL z7XfY9n}9CQF99w2*K|^)&3?n`0y=%IQ`5_g-I_yFGa1nAb8M28=XU^F#S`hIwW-;Z znqN{=^o^grb_xdpI$zxYEoVSFX>@8XNzHYsxhplxQ&+r z-329po~zFTFMq?8`Gpy4LmfbMqT&z!DnA6!9o`qvLeBxT;;EL$+L_m;upoth0-Eb- zV8D0e`54f~Rrt~G+g^Z7b3mv32tcbCXkg{S7=T0ED4U7}DLfA75WNm)?k@lxz3fkZ z?mYnA=`8@|=?UnRou1~oB%O2k8e_f6p} zK!!!17o%ct$$u z#uV-bbPYV7=2-)1uJ=>;2GFMd0qC4m-sa6dfKKwkDRcp}z++Q51JK$p0Cd2o1F9J< z1oZ59#`0L%_z|EZ_8XvcQSDd1J-p$8=KF-~9rc1Ipan z!0O#+1G=+j0-Aj(pyTsWYCcV22cZ4hV|%(lK+EY3=*DcyjX8#dcJd}b%UYUFwr0S~ zmat28E1;uN^$&k^8vxqBR)7vqj}!)`Fb2@!xg>@8fX?=lDZG}#XK9|_QmFZ-pS?Ap zgL+&FBU6|PXn}J9ZO_uwJO^ms-T`zlzDx6z-QgFw7oba{WeQyZT}S-^oq_Qw%m%a> z_X0ZG&jC6WAEcAE0k#?0$}T=CpKX+F=;GhjQRH;=>63kWqEBDt2dt|nfS4CtZzmpe)kU23p2_3AM$>U&;Ny=Kc9{F z`+$74v+>DK5L;*Cn>G-S&L+z_%f~7B`|)$L$#U=U@o^tt&L-=BkNo$NkIKeTR^jD} zRxF!K;%>PlUv1KNGyOmxJLQt~AM2-&$R*o#UM{}rBl2CHOV)d5F8*yE=@OssS@PNO zSWiCN?k{rj6|0El^2zd=e!tLiMq?^nt;WE68Nv($UX#hot9?pM;`!t7NQ%|v! zD_g!~*DJD@<>Tz$*_v_jmD%0m74zM=NjYG0E;>wk?)pM;|{i9@6k+Nn42nOhovRiPY?!TDzD4sEZCkcF zqEkGnVRoMmow~H{)UL%5Z3c~>Fzn1R9Y?tRN-d&g>o%P`w{F*J(9kjCh7TP#VbG}I zqlS(>Z_xOTBjOJ`WE<4nx6}+>I=5@xre)Wb@zW=kts0-wI9s`Sr-RycYSX3L5v*j~ znWN8OEp@yS zx*goAMf2uu2TeM2^ymp>Xj*Eq&AT*j-h$Ud28|szX6W$o<0p(5H+;yj@$vj-+1*=r zYuDzWmIoc&#r9~-xbudLJ(DHuQ)&rqT6gZ!s%7&owu<57#*GSqs*=Zwqle$7$S zD#m^HEL)+ZQRnRNvh_=!!*116#4C^@5)pnJtTokYCo~==`s7H2bF0(jZ z*E_p+Jib@y|bT}Bhl%EZ0(ZEPRL$csX|j%uF9fnHVQa+6uE1S%Fc~Hu9>YK zA2=*qHU4>2c4&OWux#V_q|w=n;*Neo<&rChWk-}RyEp3`T{YgyO5^pTvh~W^Mof^{ zz2t|{*<;JbFHg_zRi**@%6RSN**!~!Pt4v|u1s}TzRK8=o36|b*d;Tz;G?O-klGl0BnLns=Aw*-NXZ ziN))(?aP*_%aZR)mekU%e1ZGn zM?NEX3xI#Fa^|bu7WaE?y=y$I%Y@mlxLM1b>nVTlmHQUPu4wv5@tfy&y5!2$E1bJj z{@hB_Zfkedt((f$IpW9L*37ufx!2`aEB!3;y7iH!M-6mtPgYPlQ}47B zdJkA|+tMqx-Q1x4*upOR@K%B*EZJ+#qq`jQb+>Vidwo3W>GOE8XNi2b-b45NAE?MQ}mD`!@X-~Z{CT37v~b*;j= z7dO1PgL5~@=Z`JA=KqPvhL4TyFMEu(9Q`u4k23D?7Iw-o^3fsH@VvUZxp#6{QtXLFHey>7uqg2*;C~1g#IvBaJ=^=j2tgOxj#+1cRfX} z3|GnyxeA^l*9vlcidDmTwlMXn+*?CmC3*;C{WgsRH5^c1;cp=xsdJ#pTl>T;7kMebH;ce&dWV*lN9U=7Jt zo+9@dR8#INPmwE6iNafF9Kwp8BDWv3r`!RaA~y)yOKzyA$XyTBlDo+hhaW2JE%_d3 z#M>0SJ5pON?}-Zzsw3CdQ{+yD>dKw!DRLJ=_2e%06uEiOK5`2@aS=lG<(?~ejNB(s z1If=lMXm<-Wkb2WJVmY_)JX1lPn>tCvD_q2k-GtEA~)Al$JPmya2HIwV$iQ5j^U+xS~+;$Kz{BfGD_Y}D&pabQe_7u4$W4IS$(`#da`T}>B~Ovt0(F=B(NpC1*u}XX za(nMW`DYlpPROGqyLyV;+0fB)V?0If8mOmS>?v|3P%pX1Jw@(asJGk)o+9@<)JJZI zr^prR@E&wu$@-om*BR<3cciDtodq2uH`-I=W_XJ>V&FYoX)i-tZK; zAD|Pgx>MwSNsyzk2an(W%DA_u$Q=fqD0hUX$PI%|k{jVEa+g6T%U$6qa*H9}lVu%C zJVkB|WG`G7x!0kBVdQ=WPm%o1Q{-yWz&us1mZ!+Ig9gbR>WNzs8Z39VCkh{Eh}>LH z+!4@ea<6(~y#>&o-9_#?v}`K*Qxu@D#a;(CKpLdE&x^tOiizZu1nm zXP^;s&wGm8SJ0WbLXrE{W8`+P>fBk%xThx`CeYb(Lp||)hepcH_7u6L&?vcwJVovu zXtdn>o+7sc8Y7pZi8IE^zsNNQ$4a*H#EPMFNh-5uI}#K z`Eq$rk!u0*5;pk`_7u4x&}4H3H{4_7u7fUA#v46FZZ&j~+*(hO`xQ!_8-I9;T;m$f z6)Rs;Pmwzgx>)W+Pm#MAx` z8o7%-MQ$N7PdpW&8{|&&6uIfpjdHU* zMeZT!Cb?yvBKIM5v)m`1B3HH+*U?TXi3bsMyWEa~$H>*)+qngj4Ln7zE3{CqhbJD#&?32WJ#na^JLDF7 zirgE}opNt`qVRzh%jIe(ME$QRc$egXo+5W5bhq3 zuiX2dIMj8VyH76iM6DXSKe>)bX@<)c+<4?eH&xR%WfNzROeO)xGxUO7H&2lp4!tOMW_`*(!^llTu9BSTDRN7om*kduirg#E%W~^HMQ$^+T5gM{ z$Q3nk?iIORJ<(Q#UX?q*6ZOB|;2OzeJaK5CwQ`d@aY~?da&tY=xPV@hd)8CrHbJk; zec_2mZ$szakgMt`a+gBulltHE1UU+ep*NLri6<^N=qUQ(QEe5?S*Va?yPKVx;I}0ipM(!qXgXFEAs5U{r=WT({d!ma5+AjC5Ct5X7nKG8| zPft{28`D!Fx3?$Sk5DzwXqZ6M`>g)=qsPcaO>DfxGVbCja{EHmp>X?qid=VyDi5xw zr^uZNQ5(UH@B?lq$P;qbo#^2|l9^TZZHJQ#65dWu}Ned*bd zV=P0&Ifi(Mkgu&LYCaG}J?=zLJU1Z9W8Ar(C|UPq{gjo+Ymi1XDxtb^4|?JdLX{%d$;Ik*f{0 zlH11<8wedN*U=Lj2(^|Q5GaBkCdA28>icl}PnV#r6hkDE1<0*2h zpgwZ1c%uA(`pW(2i3SW;j-`%*~Jrw2KrcTv?qEypiktk@kz!Tm0T>78NRq#ZU4f-sp|1~vaGDR!&xia?jL_ZMph1?iVG{K=S*XnOW1(Z^CU~Ob z5jsxpdQV(4(D8DQdZK0wognwVC+dGWZt?z-69ItiZnjOGd?g>xS|2BYUNpAE+2Lp7r+#jB((NM0Al-t`A&2ngzTzgNP zCTO&rjYAsII)=u`UF?Y_8#I>khr_=(K{7rIouiB|dZI4}8YlO)r^uC~Kp!txb7H#Ailmw2Mb0=iUgjVA^Wpv&a8dZK9MF?qRM zHBXT{2%09>+7lNrG+pjQBikqXZNMueFY`n-44NVLpeNe&&`i0vJyHFJX372OiH8o) z=PTtJd7?0bu9EBJiITNF>z^$-25CfT23;+8y(g;G&>XoEPn2fRHFEEJqVfV=E0?7@ zU=+DV&~0VqC+d38O>&=jV(^mM z$IWt;JyF0xbL9>cvHu(z@D|Blp4b@ZR=Lrhc<4a$FM6cq3f(RFg{R2n4|f_JFLLEQMXmvKkMcF~6uB+cd(jhz5PCrFTTk3}9q9*>tLBNyDDDqiH(6ClRLx{UANHV za;FwNvNh0h$tj*9w*Y!V?oLl^4D_VjYESeBKu^hS@x;a)K|hdObx)By7sFBT*_ z7((>%LaXFX^Tb61y(Bly6CIDx%X0U7irj0^YPmN(@hFF0k^93Ft*I{b1Ig{vh4mZK zQ-xe3+07GGa%ipG*`BBcL+j+O^2FE=^qSm5o+7s%dR^`vPizeIhTI>XqJpd4m5w0E zeLQhHL2t@+_e6mNy(Kr=6XQY9+j7@>qLu@_Blm=-$h`->EBB$N$mP1x4`k2OB3Djm zN1-XSK^YJ9#Je5P`*Qs~MeYLV1G$1Ht`F!#xjQ{k5<(m0)_CI1fj*Mk+Ku%aMy}G4 zG&m%ycw$rx`b4gkCpHG!B-h^)RdVQ4xe1=A&_JKb&G8hu`=HO|{^^Ok?p^Q;$qzhH z>UF1aAy?iL{Xo!Ha{GIVTrcQrxqhB#ZbO^piak-2g1(V^z!P0Q(6?5nq0c8lj>50d zcgpyOCt9;ToZBMT*b^&;w#xPML8evxbKiLnA`o7_N8lpoNqa+i3bWQBf{TkMH3;R5)(Cby?2dO)B*x2?*uons|!bQBWDV-kumqh04lJ^h7^0R8DTLQ2~tH<6wEor#$gm z4pc$zOHZ_tdXZahH%|;$K}B*0d16o!swj7ir^tV z37!}khw97S=!p(RsDa$mo+9@#)KKm-Ph2zo=v9^5%@dDasIgq3gGX*1sEOq1o~ZFc z`^wGs!~+J}Pwoj%k^3HMD)+M|ZrNk#Rh8S*6New#U+(aPto}C!JV0{1r^wBP4wRek zi9TMax!kj!sBAzjqvd|{#7oNk=~b1h?TM=&ve%P~T!;SL|Av$S$lj8JJVow&sE^!5 zo_L=V>MOUv6SW+upWGTx49!Bv$o=Apisy;+9m?(FiTYm;@Hoj{p130*`(>6zZoDVz zde8}S*LsTFKcW6|k9eZX9y(F(b5ArhPonQou97E4L!py1?0=CvKxjvyH#9&QkMYE? z3^Y*g98Wwrpi|`Lc%mEbWcm)}F7QNG>j3%=<;r@Z0~;DFch3OIKSSP1Lk^LA-4h*4 z&}nkpJ<+ouk!$3MViX#teEmEzUI-1ByU-IA5a@Kd#hxhU*MVn9uJ^>bgY5ek zyqf2U@%&ThJd~^BiE27@mRxsF94+WX#qT6@(NE3kwO>9-Rp^4 z46?5e6uC8?cx@lLQ0{wA6h4FLJCv*DDRM2Lq?~EviC21{V)9Y{JINz&tV0)T=Eo73r&%G*%KQBO_lrJ6J`Gp`VQsx^u)`|&}DL+jBLAjB?7!$@=Q-WdZB4@ zvpw-<6Et0JnJ4-Lp)2Ix_C#$6nj!bAC&rFXqwi3zrYCyXPb<)QDA^WiL=y|TQm(%z z-iUy%lAGj-q8gelH_sE@Hqg~_FM8rterS%|SDtt{4W;kU9AEeGNP7>uRvBA)irhfx zI=R7~XktOv%N2X#-iBhi#h!RXLpR8+^~5s*x>4>o5&Ivx>ci+fl-$D;hX%S?uB|7I z3^Z4+zo*EJgKm-ghbMYapKzvcu^- zl&j^5_ky7Xaz}XL7K0Ybjqt>E1TB)A?TPCMxy76D=(0pK?!mqJV=64@!Op8d2PiprcT($P>ja^srnDPqccW zN8|>2qBMg_d&(0nYiPCHmuGYT8`4Y~Nk^gNZk{4{2=uDlVV=0)pfz%5c#7N= z&|0}GJ#mXc>*SVs;=Dtz$$j97`d?-g9fgu*JTWv5y&>1s6IC;4z1%ULc&7(?Q|??( z6wA1)1h~g`ro%gI|`LY(^065)jZLDhu)KG?}_$1v_Wo&C%S2& z_vNN}irn4M2Xgm$qWKPeD7Stz<)0ySJ>*8o?Vci6cMKhcat%E39tZTX+)oPt^Z5f}crl@i;@yZ6YIjR4>k|0N63-paL{^*HH#5r^n%I)ciN(A(sTqjSI{m>S< z5uPafp{;VWJ<)uJzL$FfDj4#N27i$J$`dc5kE5edu7)Sx6@h+|>*9&S5B)56jwjwH zgnp5`)f3MWX9L;Qt54f++{%YqU)25j*S{ZGq1LO9_Q^8gZ-0+L`-(^MW%2{Y4m-{L z@cq{Kv9ofQ6gK6oBG{bSjM%E<`***1^B1JM&l=gFZuc46A3cP`veo?ja8LJHl}M3z8lez81l^(#q&ny8t=odA4W3W^%*l8)MVW|AA zIT!xrynp*|&bz(vT1M~78Rhi?+X@|%E-c{peEijDjtSihI`uu{s$+8X?OU2V|8VMe z;hR$J<6dKO^$M1^CZDo=?5Nuh2jeg2zZ>aR_#=sSKDrZo5|1L1{>(z2u_fvnqH?UhjPg~69+lez+C_8h zlTd+Knp#FqR$hH^_Wp2Q?Qpx{@~U}v6xp-3wI* zzJ5nkp4D#@b$||LNqILGDjXqP2%aE%8?vTM-c{sEunJP%b%gd{QF%83Y9v|=9V^-Z z*_Y_@&b};d?`P-T?vU+g8CNGzlRzyX+u`!AeNTezad~$X|8HecdDjQ}kj==uQ=v~q zOwiCzSvIJJ>*k9jBtQ1&RSqN`8`qzdYqZ-JlHtEgDUk+D%3YkfCw_lYu1>+OQ7c&N zX8_B|x<35>J*kc?JN>a`+U47jPb=@&mUYFp9=ortczcKRSmH(^6)N`;aR%`d;yuJo z#FfNPiJuTZBW|;dp98-Uza(xaenYIrO1>vHBK|lJBh?P0SSz;Grj(Dz} z3*r)D8RAA_Sz!TpCtAvY}NWz&5pZ*!F{^;U7PZAs}6?X4Fv}@i!8@kV^ z__U79*Kc{F)L$PKrhFyg-#sksOA2N5X0UyQA+Ogs&E?%-W_(PCl6IE9%#e4ZtsF?C z-F6k?29tC3DqIfQLE%r-OwKi^-vr$0hr?Ul<-$$ggGsI8>n7(~6>L5`z!mt==dYhN z?ym=UEopnKyhiLxe4RLq_y+NG;(FpZ;+w<^iEk6_z^@7i-iX)4f*a#0I$ZUSB;(vv z`ij^zuC?&EIgN?`=G4UJU6`wt*&bhiVXjX6$c4EUb(fN{`;4wlx=-f_g8tl1EZ1_y zm)9cf6z-WFKR&L1QSPw9o@5=-eaDKPGgfq;aeVjgGiFUAk51u?ozC%fX1h;y$MFJfq!#aZTGE2E~D4plV_S;llH}xIZO=J*5mxTpzJp2%_5c}{~B;xh|e zMaP476&)R4Q^?h?zZl%0k6Fkj}ndnv^ zxaQr{#7~LO5WgorODxX@K1bY*_&l*0@kL@M;woY{A{$5WRW!LGBvG#l=?BzNi&`;#dt2@isj$3`b8sW$h4 z?Au{w^uD-JUe(g1Dt!@CpU%bIrtna)(`OBzE8;Oza`ks>0`By~^%^gnlH0#Ja+7^OMh&+cF9G226}lRXV2DUj~%;D*^c7Fh@2x;vM(W?Kza(%>W!BX z?R-w7nyu!_m53`4R}t+pcrIK9Mv-3oP23DWHPU;miL)y-$!FJQ;<%zAJ-!Nm|M+^L zD@QOq%qLXa`E*S0n*YKkT46V9k>TGIXm*~SXET$gjIBPeFU&kEI*JtShq&#foRk4% zwL+v{Jmk_`!+&>DZn%^hfMv5|T%J!)KK64phsI`2AMJBUw;;*+D7L0JcSUgz;`PMd z#2bnIh&K`KkgjBFk~_;Na>c<-^~5{F$#r`m4VrDpaN*jy-08xtS^B~qcv# zUKd9~y-43x#x59Z=_Sv?A)d?7kpw~n@Ap5OudA;oPhNuQ-Z@tKh&^JX^guW%RQ*Mp8yk0GM zM`Xoi%XfuVUfOTn%4@jRD*rSDaaO_1YYfqjLK%(A?Go3S&QtawCR<5zcidxouIV9m z6YTUusa3^QsbZIrl~=tpBgYIGree#kbo)t*u?g{g({uY2Y;m@Xz4`oT)A^}H!(r#b zY8TQ+;ddpTN~}z@XJHj01(n;4Xr)e7Voa36`w;CJS)bUS*nr3b$TcL6 zAvPx3v!@BsMu_$$+8zI_y?>hA@>UcvVlQcq0-MR^rUy4GxP`$j7R8+w=W1HNmz6U+ zl>^Dctr9Z1_DkB%$i1EW+lzl!@+EcoO1k8aBVCzKUSFoFA~%9GXBKCtpt_trl6I4u z!vA~7&4#MV*(upu&NjQ6J#_MVdA^?HGNhG9dA+^eP;NEp-R0~&?JKttsv&2$#{P14 z#n{!8*ZY&!laSXtgnP)@sXJKCT9SLp*-cWgR%%|aQSBw!26?Dl7pRt;ozG5kCqj04 z=JggwS2=5lTUnpiYXm*yig6UKxXYnla@Rt2*eBhKsB)SW3;7uT44g}i=I+9=6Z$R?V( zJ#>y-cW7TZ>zAG=HxRO$GOu4%be^1bGc=W(1YIaM6|!qPuV2=3vD^(fD{%ArZ6TKy zB$pr$khI@dFrGDG9C^slIsasT`BL5h3=9Y47HIv1G-nv`cT@+T>w2G zHw|hhcNO%o++4`+&-`Gw0DM$(Db!vWt*iG5IlJ#W$l255X*nyz50$gdu;=79L-yd$ zyC0xcawOd0@t1RQ^(xt4=G7S^4vDusliPEj7K55|dGA*FMdn(;r#zpXeyq;G7q~mzPT4TqGXVTbU$7`mMjF3stN%wx4u{9Z52zt8$^+PdeR7 z<(<>^+}k@W2bDrd!Am`>HK_(UQVv*M9Ns%6E+L@H%!tG`8DOqzSS zcsY^kSn*mSb+O`2L~3Hi+lbV|iYu}%YGH+9-}PDC3c;;k+?}|QIFR@e(W+~-L5rsn z*`wkch?|JEv7ZuG5I-ZnLHwNf1@Q~wcH)=B%ACfphz*Ee6KxEEf~B}8@f(WD;=#bT zhzZ2+h|`E$h;xZsiA#y!6Q3ng4ymvCN8)DEKM@^=^=D!=B8RxR32_^-1Myd4U*d1X zVZ`5w=MuLQXA=M5oEOgr{vUU&Nz=uQvv{)DyobBDfX7tq#u4YO=r$!EFp~b8uUN+a8?b#3b`_ z4HW#IR1VUrGB&wLZ_+0jn+4ZAYxYfa}O~_u2+7RT%ka2TxTY}pj9Q`7Gqpcd0?44cb$&Sw=uZQ!EFg{dvJEs+Io34s-njtSJ@N4C^opd!8Hr6d2n`* zCr7Jea6N;wJ3raEfx!(9ZlqBi{wvbhWRh`e$Y?dJml&R>;j{n$xZ#s_q?h?JU$XVr zT!lMr*=|9>NA4I%4WN*Y32 z<*e~%&E~wm$@G(4Eu2;E^7<-GonIxJBYR3(L+B4VYY6p<&%Tv<ig{E8>^N9ez*@i8^06PDIe)1Nlp$>=yDZVF0_BJ!iRFpK z#0o^4HzHb9ponOX5{B1R6{tkqM4D=cssg(bE3#h3W0R^t6@*m&89I%HfM+=k#b2Ddr5Ex~OM z&aoTG#@L=F8(29wyJF=!>w%YKY?ewj;uF^)xQ=1w4L{48 ztff2p+v0ljDWiKjQ`T-J>)PtWC$HaEQ$}t$>ArF!`M-jk_3l_+P8t7KlCwUYV_baB z<@vgmtkq;U+SPo9>PCy7zdcvE)aynJ*Ceg97FGz@Pud6?m?T4bY^n6rxcLI!d9c;k zbzGiLPoJe4+hnUw#|NsD?nH8T;_<{9#50LCiPkl>2hlcjPhvCT>TvlPC6`~~iv85& z^22rII-MP)U4Kc&`N1s-ZfS7O2KS=qKke~~+bqnj{@Zf?SeBKP^Hp>Z?V%3x01RhtJg}jwIG078C0duO-$aE++0nTu!V{TuW>~ z+(c|hw8PVgXjiE^-jh-i^p~koktQQA<4KUxa}dI-3rOP zb}JGjxmGVtfD9>O^^cqv-^IHeQ)dk>R#*m|3R;{HD~G+?7-IKz}DdNEFb%6Nx`sl!PAO#7yQA*6N#;f zT(hnXv6$GFXa!F@q7^)c5bd6CPqe$c1921aP~tY?VMMFnAD*^kIwD#jIuWfUa|Cf9 zu`_Wju?x{UHF*IA@=HU_sjxGlkLrzPcQwvsHF*$z#zKx_IW&hFO4 z)eWv$aCX}!`8ovGF}R+=^$pIBuFWyBNMATlW*!+b+Cfj;?T2e$PR@p= zc~b;85V~G&D8ySKxUrC(-Ms#c3vYrzHZ*;M+zg1fKX5iQeWTnwXm9&dWO@BHhnpmA zXqvY?aMr_mvz!f0^JWLmhNkDr+0Zm^b>M7h`W87Gn%-Z|`c-e0%W?a)kh9Lzd2%(N zgXJ_dJzugZvYljG=r*~-p+n_*LbuBu3w4sSp3?<#HZooC z_agML+!|<>+y>|oxlf?0<+ea2a=$>=$>q!NhK^hjbfa8N=ux?XeM9CJ$!5@FlJ+MC zZ4K&Af}Dhj||gTnMg^ zya#$z8J9s%%RL1>A-5WOM(%Z+Dz_0@Dfc<_oZOGlvvR*ftK{s@7(OSrEA)z7EvWFk zWPNa*WOL{Rxi-*xxh~L)az{h&$ejqSlC!^j_`cjo=q0)F&_{B`(93d{L!ZiB3t6$A zcQ+TnFC`a4uSnhxeIvIVdR1;Ev{jDp0qJ)p=Jlr(f0EmTTdRCuL%+)L^&I`qL`p*F z54no`|C;jg>YB@xSNX@wW$FatB^{^?l9CX5LmAsc738`@>*e}DmE;COZ^{jYs>qFn z-jbUHRhOFzy)DOesjpIXWTV=-Zo5|@9e(DJ&FX~H;?2+G4*0u%$p7k|rT^-B_kX`F z!s{vj7r!mSGt1t~FS1*SpR_lg&dR@}X<+>&O*`f<=>etEV@jo`luE~G+Wvf?ZNNRL z@ad;YrC%?V{=8KB_fqLyDXr}%-J$xW(rtXYkY+f#RE9yN(&I~|FE5q8xm5c8Qt6eY z(r+c{LfFEuOJ&$mDqWRAZs$E}Tq@nZRJu>8^w3i2NlJ76(<3mWREBw_(hrtOzfdau zUa9nVrP6#A^DjHV8_m{c`PT+CEtTQ$Qt4w$rOzmpzF?=ceYxa&mR*+`T>4{AgqkFV z@Ua(q?5EI!mbg78(!rynNOvK5H1R}YPvS@-f1|m$nAn?W_4+=<#YF0H>N@C0w8!Q# z#7)FwiQ9`4j=cp-iI1Xw09-WA=+CJ`1(b$YHhiRxuk- zX5LUIUQC=#yo9)rNTE}_j5w9Jns_O3Bk?lgkHpJ~1^de0G@ur7If4BYKR-s5)* zh;4}ri9Lynh=YlD5GN4tq|H=34Oon5#?`u(!=Lm=872Kui7WO~lm4j0S%W#rXAQ~3 z%@1x#aMq?t@>yL!$+sdQdq>KuuZgr9D9N}XxQ)SW4sJ_u+k%+?L?B2WRDDvb>6(k~hy1S1Y)>X}*GM7G(2OIx9|XmLk_NxSqlF z4Q^m?gM%9x+}PkI2Ui^2OcDFfSaFbZL&o{REeUREaLa>R5!~wF)&{pBxQ)SW4sMH$ z<5^}d!XOls|%;0gnS9314x;Kl|wIk@8B zW(GGqxVgd24{k|tOM_dUkd0%m2y%7ExHh;A!EFp~b8uUN+a8?bk>ih`Cyron_Ml6S zR$WNtAHNbXNPGSzGj|BCV{ko#>l@s_;06b0Ps?N_V}qL@5@<5cVD^NpbzYea(Q*3?k{QGh#$tEzQH#uSAtfJel*^7{onVt z^y^L*`$^jz1CwM(k1ds+S}J`*sq~V#WIbbow)UEQB0lLN?_!sBuGrsAx)alBY3i*o zZc4osb}!KeUhX4aO}wADh{&y4Z2g)K5StP0`>aXT$SA2AC9c>SF^QZRGR_WeZgBI1 zTN2#T;FbrsBDmGTtqpEda9?Sj1XVtqcayCqLj+_lsZI!cOs)=$Mrn07K zGE8-zq*dyEkh5W`3+1dTXM0HJEOfElCg>-*uc1rjY?$h2xgF4Sxr)?Cevz|bs#$V% zp=~&tO^v~;C2g4MS7o$sP+liz!&JY?*)Y|Oaswed$jLV-Z;=~|+b(C{puA1ahN=FL zvtg=5ayCr$r<@H_-DSUlC>f^OA!)-@_sXq+9Q%;hpPYU`&W5QnayCr$u$&E3*{gVY zXTwyF%2^#OCuhS{Psmk<@^Usz^|YLZsme&&Fx7LCHcVx|x+(8$m}-?=PpF)n4O6`$ zHyE-DEbnZXYMq=7Q&o_&VXF0VHcS=C*)Y{Ra#pn}GM5Zfy)U^GSy35nnCc@r8>Xrx z_cHXUoDEa$B4@)?U&>iM%C7O`8u zseV<)4!EjvouNPE`a;#@=&*2^3O2_usJa|o1g?x6ZFv2e4_dTP1-Y3}4dtUD>?+C4 zw_&Q9l6NAjNInGF`!sp?I8j>4AvtgQq7H-$JmVqzzMbRYn`8Y9wdFR6XRZI@4IrhN*hVStX{4 zoDEa;le1x}edTPJ>UcRDrrJ-=hN(`HvzkkxsiX~4og!%!mS%D`Of^KV3$(wS4O0!5 zvkJ=rayCqLrko8^9VlnRRHNjIAuC<^E;)3LoDEa8kh{4xI8oAusSc8~VXE`wmP0M& zY?$goxwTNM_{dGvGd=+Ass2WM&ZfVwXZ-i5&VPRg_`gSW{(DrX%KxRKI_>|}!utO` zsuTbEB`qo|XRSqbA|LA-v!8#`656>%^&sgkcpl#xDtVYVlJq0QVqyu=T9EX?7cVA0 zO0-7KV?=At@USo5L|jg^is=(Xs|-D9-QxBLehOh@B`b*CiBA&;5}zTCC9Wh+B|b~M zf%qJ83GsR23gQby8!DkLp`nsh#OY=fc zwZv(p*AeFuUn4FhzD~5Ek~fHN6W0^1gYZqFqk8rh(S}OiCN?3yL+n6&m)Mv19?^zM zsMu(znvFNPLm_5z+e8J|=DON2 z#}U6Ejv#(XoJ{6KB_bvb+t!Z4A!ZqsmuM zQ!>f8J(X%3CC+XWWmIiHaaIdXoZUN#vua)9tU{K!4#9N{u4iz4MeIL+x+TcLA>+v4 z>;_C0I61iD;ARFlJGi;Q%@1x#a7%+*Zo{IMku0!pa07$0(j-~l$l%5XH#xZCglt%B zW{|T(#<{`G4{k|tOM_b;+=}2<2e&r34Z&>;ZZlL!_GC+t+e1bx*OEP{=qVZENL;Pp z>V|yHf@>a}m4eASItJG>xV|+|1x+2RApk`N1s-ZmB1R z#g+%TB4k`0+}hwa1h+A`&B1L6ZhLT!^3)$ePss>}KZ53xVX?X)qm}8&;c6aShv2NF zPiF2JT;JdZ1~)jkk-?1(&Yla&I*NJz`324la(2i#H@Nx1EeUREaLa>R5!~wF)&{pB zxQ(eR*b~Wb&6ZT^SCb}N$kV`YU`0>K5Jz$p>;aYJs~hrJQ$KO`*h=zs2(DvrJw@z) zk?R}ez>slpaQ2i-7C1Jz$-xx|H#4}|!OabBesD{ITUv%Mw+RC{^KtstoR|< zFK>#4Q%V_ZH-HMdM}k5fwP{`PYmo2wHy;?M9n!-QE^H{RGbn4 z3uh!ObvYnm;Z&%YbE086m1J6`4P;cPS!7nGS!B=myY^wLoPY22d!OfeumAhMo=)Lf z-_O1Fx`%V_efHk>zVD-fZ^K)mj9-i`lyRhYwX%G8EtPSkca1WRgSJw}LCCerI1txb z8Ari3D)WWcMj3};I&M-J0GCe%cIy80)N!p~=!;0@)E}JeqpUxe89L@jZ*ygl@V=GK zS8>nsb8y-GjfQ@#1**KN9&(8JB6CiY*$qb7Kq*mbxI zfBYcuRUWOcTe}Vwpo76NKBrv=AJ|CfJ75!FE5WksfN6Z%b*KW%hE-KqHlgl>Er7iX zmR*Nxus&FCb#Hgq0YBTIU5A>m>^flhPrDBH!14{e7uFvIKSr%=nuGLAbI`B^_fj+A z#V~%>Ox+9r`|B`i%FPgS-xe3mR+{2$TA0Z!<6Non|xJx z(izB8!9YJ7=x-U%2@}WD&&2V>F)YNg9+rh!7H(OLWz#H^_8Ur9`%f-L{XgHv`VwgkGD zGG2`OP1%OK;r3Rz1^-@Gm<_LwRQ?Nf9&fhfd7A?}*4)L~@2`LFV!c@rPQ`)Y%T$d2 zXLqr>P$%75N%}&wlB@;WAGS7ZB!tR(BeZh*ZXmemdBI%_3a7nYS|Jy=$f z^FAY0kS)OJ2mK9od*)qM0)f9GM zKg`DP&=^+5GM=o4`B~;~8P8f%wuNPFEDN!WXRL|i(WU3Ui`BqL8_0uhSgd6UmL*x1 zVp*zX>6UG`Y`1#$mNysGw_TuHX83x#u|Cx%^JC?Le|JUXr@N4u8i;eAZ4tP?@^WyZ?H1f$hDNQ zMrKRS)W~&|u|^J8#ztd3Wxnu+D$^SIL4|y8hbd%@{D?Bv$itPfqJK;o_clTqSJgxr z-_Q}tSR*%A#CWbR_^qs#}r`EklPJz#(`KX_4K7<%ONfI$j5Jz%^Ba(ci} zWt<+s4=R4%3f>51Velp><6W#{ltsgvsEpGC#wp|U03H+bfaM9wI6WXn8K(zKM%NpQ zIjAvNA*TmSSH_`?Dav>k>r7>w9>6*nKUW5CmNHHcn5K-=17<7Z_{DT(oE|V=SrvF} ztU4W>9eORaeBZD$~Zk>t1?axU`1{2V%?#P zV-T~I{R(f7G6d*vSDWbp`;=7!<44lm#d-igXbQI>;5>zo!aJmaE#S>p#_0j?DeD1m zfwF$^K2R0`Z=o_y4>+l88a!5CW_rLU$~d&JNExRGe5Nej>3B$gyV~5v`lZ6{Sp1>} za(ckm$}ptwNRibQoOSNNU^@!wD83`ov{C#Zx0Na!Z839v!1Y#rlHKW!a1gJnCiIV@X@Prw$yJ_*a#aSK=$nvFTgRmA3MQvXF9`H!PgOfCQ`3H z;u^JHeGGvO0q^D=Z>~Ou0>l7xhvn7B9j_J$n=%d3y0VPj#(z^1@H3!4Eu7M52ZBVjo} z96t_@Ki(7mHjEjap=UZ4h9$U{nvR8GY*d=KR9iOPvP{deEaT_Fl+CdWt;={f{mK9od*|H+b9C)WqS$?R@N~&1qYnh*A{+7iDSlGhCHkO50*26M>FwIKBEsL~_ zA6K(6v6dxRmSkCq@p$zq)xvZem}yy-W!aYHSe9$q3Cr><%eSo1vdfkg!HYM0;=pIx zY&F}fhVjHP%-1ruTn+QLEWolBmbI~rrCD=vn<@QNXw!vi?uAlvLwq=_!}oCVXB4cHZaq&EX%Sj%dsri zvJ;l&S(a~Ep=FngS#!M|$?P$jti^}W?U{>pW&`V5#uoD@|MATnyV9!TX>kA8)n1H~ zz;w0GX=|5V?U%qzSNozecC}wt#+mp(DvO23PNjLY>(9zqD=t;WuJ&)rGT<${+10+T zkX>!jKwh8uOBpId2fy3Qqg_i?q?Zr2LK(Z--pbh3Ua5@NXUZ#MSDXD>Gx&LjGGFLa zW!lxQsxSaxwZf+Ox4N=mcxlSm)xJkre|WDbV^_PDvPgKWpv-R>)lrrJy+#@PHT9J7 zsJyC-oAjVE?P|ZKkX`LZ6lTNAP{ywIW6D^ItyOjg|29!p0B@Z#cD0);`yJkTWq;$} zmdbo^GHy`Du6A2xe(*Mexx3o!6|$@Sx(2eV-B}sC+HWXhSG$|CFnFwp%}o5B%A&zG zDPvc=k1}?(H!I^z`~k|?)#k36-!dA6o1fsaPRdfquJ%x6tckWNV^@2GGIq7!RK~9M z7-j5gZ&Svu_Bds%ezq%PS9^l8D)4qFV^@2!GVN;bRLHLObcO6{?^4FD_Dp3V@OCR> zS9_MSaCm!^v8z2>8N1qVDPvcAzA|>T_bOvodyz8MDtw!r&Gdm$ixsjm*{6Z*YA;pB zuJ(Rq>}s!2#;*3;^0zNKp7>=F?Rc_Q8D~149cwxN@sZd6PD=iFQu2T2q~yN;-~2F< z^z=iIjpqV;Y>(rQ6+B;2XvIH7$NjslxKBcRY|*g&fhWL5!cK%ufSm-(9$O47uV+k# zWea@@ESus}VGCfV!Lr9T9o7fydak&~HUl6KAQqNAwwbVDuyL@_u!On*bfn8ACV_OKo9$OMDdu)qf zQ(==~vtVC@WsfZdmOZw`ux!D-1na;Nd>NKKwk5Fau`Pvd0lN&g2du!d$F>}H8te+# zB-oY3J+@T<+W}Hx*<)J`%jzi&)*pwAKe1^#RP;=T%CH3YQq!R_j7DgizZ>7W%-sBT6Wp8BFh|jBTQMo=O$m3VivFW02%044E3fK z!vZX8VGFmhEX1-Nmhp3A%7$AOX&FCFT#6&89`?VY4(S@0WCK%dV5(*5mStL&Wm&dm zIhN&GcEYke%ktThGb=8%@Ujgovdn>xnu%kpgdWGRLe zqvZ$7bT3wGn}*$s17N0mu|^rY7w;%z_hPNGSa@s|o9@L%Wvn9(Dr5IzlQMQM4&Cft zY*ona#bFI(_hN@K)(l6K@k-tvW%=-qDr5IzpE7nY-c!cz#Q|mIpgBFkbT1Am^MyX9 zOuHBFDGUJM31p^!eV{BD`U7R`UYu0M?!|Fs>|T7LEE3*_%6Qf4Gi3?zcp93y6=tK^ zy)YZi?!}iHr`?N_8pt;H*9v)df252D@|-dz`&b#f7Z;VWd%>?F^E}!gm9cyAi86LC zepcqgkESwqFMd0tMw zvodxsSYeyz(RNeD?!}MFzJk|NSs}bj%6KKOk1`Z;{G_ZbyaCF1C6Cp3c_nYC2J%YYFUoi&Z-g>l$@^6qujGwU76I>yGG56Wr;Jzfep7~zmt%r5Udg+v zj92m|D@*5%zSk742b`{uSMvBNHaGgtRK_cL*OlRJ8^uaOl-&*QFJ*e8?_!0R$>O-75SI%aOO>^O#}f%ZFAi^oG7O>P z))F!diaS;-!>F~Rlrl769cz@Kh3W88hBl*Ptui#~9Ho_^ndaE&gZ+2v^Jtw4-vHdC zfxF>(D|;K>R%N;H$|(C7-VSB?@X9K?0B?`7EAYxG`xD+iWxUb1yfWVCdqA1q=vzS{ zZ}dH+kT?2PRK^>9-%}P0kCg~!nZf%&8E^ExLm6-MJ*kX0`c_iL8+|`f#v6SrE8~s6 zpDE*wzEzaz^Ju?R$QymDD&&p6Un}E{zIQ6)jlSoU@kU?PW9E6Z7nK!&Ra5o@ydRbQ z4zId0K9BZiWqclO4P|^D?QdW%Cx7hRQBxtGM|)iZ`8?XYrFI4~5T2i!xJs z1OI1C>HYV)vH$+caPz8T@qNHo@kM?P+~2BSE}_?#(0fbh4@>B8OXzD>mkRr(%Uo!HEz=wL%2<&$9|jMQbNC5LZ31E|Mus`q9(VW`iG_B6BvSGWzHd} zLHN6ZI-jpsFz#{d5L7yJF!&l+4ne&N8wvXwYyxZsEQg@h!g2^|9V~}5*28k}Vgqae z>_%7)LA?&^gEj=d+~f6@a?-%(usUhr3pn`zd9YVtzl1G^v*j$TFYH&a0kCLR=`G7&!}f>% z1~wA*Ti68H0@x+6=V0-EJI=%6s5;OH(_5CmgGCH~&{l6*{vK9uS^fbIZ&@ycsjVSk3@Ez7^a@|NXaVRK;7#?xDte}nxF`YLP@>@`?E-Su}^ zKHc>?EL$#r!1{9t?hFpE8Qr62M)wR$z-QI4B)4a7Z8j{`vUD4lX<3$KJQGa59LsWx z8J^^5VO}wG%X}^Kv&`SJ0QIo{74%**Q@D){46%Vd zEaNF@;=(P9v@F`PSj!SDOR_A*vQ&>UmSS0|W$Bh>T9##5wq-e%<-&_MTX@34JR6vAS)pZ@Ei1CjfurI+f^N@@?zxYk zWqvl!-?9LXT9|GF zGcC)qEZedi%W^F{VOgGK`IZ%0cG1L*gHxo?HJgB}Oa zUCKCs?yW2qp0BbO;FVXF0-}V=yjBp!&k=x$|}RFr_2{#U1c0VulS(C04|pC9WY?h3NG8z zY~qeLQy%g;*M4t@|NqXlfBgUb-OsY7BRUk^S#~I@;4cY(d|}k>whje8blRc#3zi*< z8?ceEf5Rrg;-jb?ic+xbP z{s`azv1#McGi^M>65LD86irN6#=n<2AtE`CG^jp(z|}89P{?{^xV<=5=|qD&slbQyI_c0A)O<`zYgqeN35N6dRzB zhx2iTP2mkv#sk|}883D77gzyWxOc1 zT3I5zcFH*CZjCZt6bn|yi(+fz6=uS1uaFnTHY&@3*FhOCifvMM3SLKLyePI+*>~`` zk>=^QJCv~v+gTYeitSNW1zs0rHR0`3rWeJ!D&(BI0}7kL3sKe@-XUcn@VY7EMX~pk zg~JO~#*1PfDC0#j9w&2A?4&YY6zidk7sWnN#-?dcvMM^~?lXmKkv=UedfK08WTR=Y!+XJ>SEC((zGN%KVO<yJbB8JKBl z(K9VA!xG#}O-su#e&w6ER9iOPvP{deEaTh55}1XZV4pP-g*{t@go( z%&eH7W5awcW4ppIf6D?aYhf9i6efl(+u;qtdXA$^tUj; zvS7D+(3!=@KD)3o-#)TRhq`@O+T5B_UqZ0+|^#@7BhWwG%3DtiIm zMP)qo`zd2<|3_tP?f1uz5F#Oa@INbLYkz)nj;2(d^JbCd+S%; z6VUw1!w*~PS6&O)Na&WZ39zkTm%z4$E-|NycmR!ojET`RpQye^J1`m6xj>*R>t#U zh%%lR<(0+43s=VT;tpjg@P;bmc~Mna2E1W6d%@Ke^1NWj$~@%d9%VdW5z2U8)KZoY zZ-g?Q7j=~JycnsBz2JJv*b5$|jJ@ComHEOOtxS8tk0=ZP9HWrE;K!7)7yPU;_JW%z zV=s8DGWLR-E8`bjq%!t`TPjO{H%=LQ!EKeX7aXOGz2NrBv==;H;TFKo3i)Lgt&F|k zZp!!-HbEJC!9A6+7d%lJClvNk#$NCwW$XnHQ04>829;B%R1H$*2R#{o?q2Xvg@FK5 zG?1O^5z5%XWWUxt~{bmHmW&=PToPPl7V`f)^=cFL;(R_JS8H z(_ZlN3fT)@s*t_l7nHFVyh0g!!Q30CgT3I@%EIBzR>ofN8fEMS&r!xZ&(`_(-mZXeRL-#2| zfMb!evhWTlGG1g^qKs2Rzf_jasi8|1t_S>DA*Y5eQ^tq9oKwcBp`wga zLoX`h1r>gnoP74V<40w@jIu%*r-uHl%!#W3E0tA*_nWf2;jL1pQ$w#S^ao5;*cjel z%G$tNt*j%wQkA*7-tf|t4TR^dY&1N6p7FRVc;%JF!b?~70=zqv;qr)MjWU5(wX%-v zXTW_`A!d&_s%zkGc&{nLd=1Av%6L&ELm8$>IBF@&hqqQ4W+gc4D8sY?$2w&gq;}L( zhS6ikdS#p%`k*rO;2iNA6rxk+ctjz3O^%JqI5qS!W%$B$ysoS(ye7)}!+S&75O~d% zMZ)8$jC*k5wN#b>Z<8`k4Q;Dz3B1k9bZTgOg&P33DCE@8&dRdkWhr|XUN>bY;B8fQ z23}8P1@PWf_5-{=%6^B(+60fgf;T`Jr-p7<#;Ku$!0-X()X*IYIW=^s26AfXPGwKR z8=K!{Lon77cHYvZ?SUC`*L*ma>KLCM#PGZ?CDjUV%GZVJ5t6 z4crEArm`G(`;;AlH%r+mc>9%o0dKal@8G?y>?e5hl@-BbP3m-%E{EDcSrvE(l-1;K zKrL2S7w{d055rrktQov_m9>VqLRkpBgUWirTdgb{-XUe9;H^;>1Mjf18SvIBn+K1T zIUaWfZzEZ}BNgsZg|ETeq=8xR-cz;<-d1G?;k~cy7`z?IK81Ho*;nxPC@X}QtL#^J z`;B@g$I!iMlZ)WApKy{D`Nyc5dW!TUg24|pe)^`oaO z0^Uc;#=`qV*)(_`D~pHsnX)8!{33HYUM}wlbbP5WokbL`hxfIz?eI=3+Y9fUvZL_M z$eL)3l%9smW^yjdhE-y0r9Yv$&A z*u3h$6P5o&%jVW2QW@Xzx9SH<=%#KRUwm1lYl(m%CG?~cdTt55s)XKBLSq8yZ5xF7 zntc7=K2aIpG&d(I<6D`p;IKV57rkM_VEe#E!}f(ugzX2r92UI5#Jrp(^b{K37>~L6ipd(<}X&V8{p2A32 zHcv;vehNDpwh(pAUkZz(?YVAu$y5cV9^mxmn_;4VRys&^Zeffrw!~|u)SgT!j6W`hK+^Y2b%)BA2tK_ZP?wgIBE%O<{yCd$7%Tm zK4@m(nx45LX;^}LsTsI7jKi;A@L6h@7;Fot+rUiAvMghrU<&70#(`MFSX0oepuZSz zSiWV2Htw=zMg0CTfgHIug;}u}R>d-o&Kkz*#>Dws#$j5+T3FV`vJlI9SjNi9<%eSo1vdeA{)8dOP-?9M9T3FV`vJlJoZDR6;(KFNH!!3-ofzg)5T9#m0l4U8DrCOG5S*B%KmStO( z!}H&4O|FF}Y+#;c`IZ%0cGdVKg&2oZ5Y4lP1zQfwXrP3vL2SjhgleIAsYrJ zVYFqjmL*u0WLb)3sg|W%mT6g*W!c8#rPdq^b8X-W%knJCx2({z%a#>c=0K~#90fK| z%#KvC%-1qMcv}BqdW(etHn4?dZ7d71tcPV`mW5juX<4*ov6dxRmW1}4dka%6OtmcC zvP{deEX%em$Ff|@PFR*_S-xe3ZV$7IE?Zb+1084}xeuY+!-vo^U&}a3ZsPnc3$Uz( zWo;~Dlg+#(J?NR)MPU}QC1(;wS{7|ttYrz7C0UkYS*m5}mStL&Wmz`ce`afPEX=im zCoIdeEZ?$1%Pw11WEoqOW=DM79*&@899P%%mUXbH`Ne-cOI@`*So-y}1pynF6Hk10Ra32}TC4`hTHX?1i7ohSjXVb%v%b2t8QKpY|N zkay6wy|pikD?SNrvFsphAJ{{%V_*-%&V)Sz%gsCr>krGXFw-WcXWGPuCAgQGHZfRx zeiK;8Hn9mzw=C1LEXxjBc2qs-F{5;~dlwe%c?xoOE?&RB(TUT^@pK^mBlYGxC&$-; zXWqd2@XQ;?Hx=(5-h{?*-qAPp33wdZb2_-w^t>IM(CpN>r|nX{y$7_!D0uubaXO;h zo;Q5F(fCWl-y@C=RULBA0%yhYuRzwu-%@#Gf%E>}4dLGM!UXE-j?z_&3D?@mh$@b% zRm;I0TZO+$7Beo|;pHf!M`@uIEYEaM%{GV_{puz6^`wkgyaMhgaX=!`N%{2GgtH&<}&MafnjZ+_Avz zR&*p6L&r+Xcvwu~?UwCUPhuB38#wW1c9D$>oweoYLTBic*doU_db036G4I`Qc)PJy zrw+uF*W>>J*jLJmkN+FeGs$^ZdF}>w(=lJhB{?6s(|uF{`10e#^;eUe1JE+Y`NQ#^ z9=gE%!?M%xhk8cOb5if}js)i8p==LR4w_|rV4o$1od{b6b~0>LSe_euuyN+l(W~Id zvFso`uRW!uV3G6w|KPX!@cmf~uumhGc zd}qv#(yQR$d2ATZW5Y_-_O4g?feNK+IZ8)+xsNzLgZ6bEB}?XmBl z-Q@npwMyORs1aS=T{OiO#FGI`Q4r&?zrM32jaQDl z{dZ}(BgHv7p3^r`zO5G<3FFR|-|6n7H$H-c@1=ewtjV|NfRgO6pzYf`w z#$(BP|G0ImQHozYb@9$X9%gHzw0?g)a$C_`-_*XvJ&o7`6Nv0DyFpHerA z9G-dY=G>bl9q-wq#$(a;W~J_fS?iW##KTGm$JdlJ9!t*r$CB^fEa*7tE{XLU zmqlm)W6`r{XBWTtL(XQU#$(Av|5)<5;yp#w$F`*LP!jW|{;{Y%NGN*B7BwDMy80hW z^5=GnU;JGIv$@7&$+fm*Jgb6RKZ2|(i(h`FqPj`qUOR9*ge#uZn6(-fC0);yiS-xyED3L;qOPeC*BobqcO2%E`Ex45KUCoKC>z3<>*00T?!dDMJ$Hov8=+>Q633L^N3F#L~KB$}+nud{ZI z#^V})L$1<}@s4Q62sB+sIU>;vetcK&<<8|n@%f0qk0p)I2)eV;rNu8?pLwoVkI&-v5BT=c!S4^6eOH6V9elP0uZa3>Ma%iQ*FOAw z%Cv=v_YZhzmG^fsC;f8%9(~`gKhC}KXG5=h_SbrS+e1?mb5e7=uA3WI>aQke+l;+< ze_-g(2ai4asH=a?leufdCjM}+eYrO;)tz;*O-Mrgf$>+JQA^5wd+#S>(@*|%@9+fg z#VZe{OqlTMtb*w^1~<9$-ADc!Rc7JAHX)Dh`>ogdr3=!p7wblglB?f2;@L<49=kTB zbwrz#`QI$qpOOD_=Vk5o=8T%rWcD|gUV3Ww#*ZGY`QW6pD;LdMo;au5FTH+?SoZ0W zEB--`@BV6_qj1ZO_|_HQeDFZSH&=$0o?PqgYMtvouyb2+!qWG=KBRHwB||{@Ay+p0P8%+gCBa^nd-y>&er;S(SHRhvFUR`}X<9@y`|x$!h&UtK5io z?-$&$xOLh4|Aza z;_At?`>KCc>(S#c-sR=BWYLyS25GAVCdnm#V)u5R^OjT`pV(6j9>oIL3>a$iIL8kJgo8hmt1%1=*x^VQSS z)~u{M@Lbj6`1{VqB~R(PtY7hl^sD68{6|+@{U+0Dp82X}YLl$PFSdPa{{6#nk;tsz z%Ep-!4xd<4ZCc2ousZ2SpKKb@a&Y#fKC6rMv;%!Ec`y9sv10vB(UL#n=B=t!tl#L_ zJ896dg^~ZHYY*wxa$C&yWyQSoc35W5?C%KC4*YJO1M_ z%YXf{cYHDEneFrbmvtLv7VDCGG`zBFOVfvkHp(<^87$*T=UZ#Y!peC~|pYrmeh_n}>5KXz8F zQRCP+XQ{vcW-ZlbVNDBtn=9eUt zgv)Nmd;@|C^cM0r2$$cCxvK-8H*qVe55n)+n=wxY0kJy1i9$j66?K!01F3KuNtCL! zy&tH#1q(RTaWnJ~NF{l`if_Q~z2v-t5qrtzrFni2BxGcZr+7v)!5kv#d_R zf-zinubi*v-J%9t?Pc)SHezIeIX)Ze*Sl^4p01Aoe!XxU;h~Z5Z8QNtpR3|B0sHHF zm#GnkL&zQDtCDVEh1OdKN>qLCdwuy$RoY7jR^UA`cOk}N_#T`|>-2zBX@Cu61D*#V zp?OLjvuwno(QzjIvIbZ`I|tPeP_<&k`X*pw`3_}FfLB8Vu(QKs{Z*F>;U?gjCKV2t zfG7sA$HiR?E43dVGOqs5{g=8*PD8AIJLG*J$3s5Hum+b^@EX|A6z=dK3bUIq9Jt(^ z$U+ma@>))o33!tM>L1#@sxxl?S68#WX z&(=+KER!W+=uxupsfeZhOyR36>=|%j+0l9?VA#V5VC(ZilncIdaIndc(8i~g2{_IG zHpKTKVCbQ(z9wM9#Z_tI=a1FD3(m7;RpyWeX4xa%zJI|KUgwX(Y*4>}fNdAPZEpg8 z3I8m}1l;)u0@!Y@gMh~u4{K%ucE9r76%!E00MG3UZ&`k*oOlGQXZv?53d>8czitX2 z{d&kHQ@C#<6lN=T2m)4bIQ^Fi==OskGBkP=0c^6m z5HKa`;VUMC|Fng#NW!C7JsYL`Y&84n(owUl_SEVJP2n;DDC~LgKbcy+wF#KW0Jbo% zA;TACGqE37m7{h0^2bcTSq6B%_hI{{4UmAxu=-aJ&dCsq}#630+q!5Zsxz1$FK^|-fRjZuui-lRo)z??%nebkK4*cl~rR9)##74ofO@G%*z{so7DkJno&`rH*?EmoqtT^!6Z~ zJjg&dx!dL&*->&Ed-so@>RxgumTKEji~V z^)0y`??w$Rx}%eB1?s-y4XWcN{+2XxlSYpzvEAa7r<_dCnI zO}*=T=e{2{uYw8sv9k;Va{jzM;ZaFyiXg8p@;ZZR#=keGg9)n9MSfw>s~a1AF22nW z)Ub;@(agI;{pMZFPKLsm&s+!yzq4+V@Gi2h8A^}7kUmFFu=JEJa*5>5ZI#!^-OWMf zc9EdwSm`SfHFNJaE0r!12joooDWsfiV9@JbKk2F|K}HT}h@37Cy59sf4UyWd5%iwV(*@G0HG+DD$Vd>~rn)95DnvHhpiddJAVjVf z2h}%0YeS?}8)WL*qiQ!9-3CDiLSznzW@>1H@TEd%8!gDQr~ z`Qo5J8Hx{WsZePYjF=fg{bHnlFk+qxl_?;aD99ul6e{}}RQu2SHp_PmnjR`;+Pj0A znV==15(?z}vr9mbOlyyztWa529Q1?!B(pXnl97-3dW^&t8j>PMr{Rpu3Fh1G1t}}mqE%w-6R~4XMJ*$_=5P| zMjH2!t6alVQ@?yi{5oR|Aw8s3XOLbiV?UA6Al1TAim&> z?&4j&)B>s1#rqN0+wRx}i1l(bniw%OW+=;|=OiN#P`_$Vw}de@a8r;rHB4K=r0>*I zc6af<-)lfm$?M|%P^ly-+ZDMZ+(nbL=&iXINp}QzJ>OG;x_Z}^)UMtQz1H-UIEn&_ zw|dIquHN+}FvQ!>>$9FxDFjPmLa^lbp3;{ho8q3QC5^(_&AXwT2=Xo`g(2Q|O9-Ub zfTyKVH!NF15%;uAp~$0H^R(=zs2d7#_-QE- z3YYxdOHNa?=n3KKEwy_hEU72Lf_lryo>-F0B|Uq~W{PT0LyYY$S1H0M7W9@@PeWu- ztm`dvC<-Z#^p@jKBU_7J$ab!`)aZpJvAwXw*+&NV!jfDrsn6-Wj~kH?T2^OKN#<-Pd{%ziNQZ!-}IHxept30 z%e*T0lVub|6pi}HS&HEP5TX5~et(E0ipYKvMRAHEsh{km@EriLxu4vih@d#xPdW_1 zHf$e&^<3>I$pf(D3YWP0%V{nNeg;e0^_SYuU`gULSTeT1jC=-5PI1YL{be(kR2zsT z+xp8@iZF^({iW4FhzyGB{bdeCA;rA|o^mSX4t8BB4IV%7jzN8vLF zV(kFAMA4h#g8>pW2qKl@=K&H&Q9x1S896)%+ZH$&+t&6OsWccOmLl>Q={p!<*@F=# z&qx}V_zc04L(j;0iry5zJ|m5WAZ*DHgw+`+Qz-H%x($^56m`QPCJ&S{;SiA&s|HFa z#deB!2g)*vB8u+@%2|rwp%9hBr2bHdB#I_s5=C)}A}maHQuq#ohz*k)6cH5ZVbWn3 zL?*@CVUkR7h2q;VIZe@GIKJVu( z8e$2>D??<;XuNX;qw&tYH$?VxN!>A6^6L;OGX^4(qI$T5Qf#Ma6)wxhpp5fblo=K- zXSpQ!SuB|oF7=;RSxDBcg3ozJ37-LWWB7%n%sBx0<0LsupD7suZo)xAU3 zk41Iga;PMaMf4R!dyO3`rzu)QLM$FCwId-CAzZuNc}kZm`IY39Baz7S^-$T&=xXCo zwEQr+N)bl!@GxmL4k81>)oYmf<|^xvr<`0EhdeRE+?9(gJbmrIQ9Jsw5h~K>vx@q7-+gnjV?%mQfRtX-kBJPDFGzqP>ns$TAA& zB#0j)%m9$%R|33(nKAyJbMoyVeGM##=d*gW4DY|oexa)V1EV~}UQ zJI{R{dHiI34DxIoA;~d_E<&`|(GhZ*B6u>ycO#_sWQZgPSGkd9d-xcc+mCczIWie} z8jh6BjP{*^q8&%d)ydeNuqoJ{Q6r_*6fDWa5?8W2PXmuU4ducVM&j!!|+z^N#j zH&SX$g@~o_8YP1%4nnvZj56Evuty$$DLV~$x{Z=cjP5-RMJJAuplJ}P6w60R97O?y zYo9yMqaJwzq``FL`F504nvUq0=_p!mwDhIOhLG&h_!wkN$H&0=95VG9ZMLYfN2WmO z_#84#A1#fZL*XTe_DUNqQz-H%4vv=n6m@4H&qa5hrXG2k$*38~Q+bSp&Or2bM0*8} zk!2J`6wi#2vlPLx$P+)tY|oP(d0NQaSmaqdMxtWzzU0N?9m^dfJ1OeUg!pxg+@Oer zaNYZ?$YA9O(>5SNE}Ii#mE_>LeH9k!kW+IUbL~feFa8 zYOK^qfQY5Y87qS+4nnxjx$|`O$P*%EXCaSIq+CkC&Vv1gW$TA{N56)17C6N1lnYYa#M{IYH7EVtbrP z*dFIZInScKlaR-MqBKf^NQH3q=G;lVCVS+WB4?72Cw8LjPePu$i;yQ{qLf*LqA`n* z=h#FEU4$juvBdR@JI{2FJkQCsMaXmSBst6I;AG@^YLe7XhDf3qIZ2`@PC>X9Pcqvx z(<4uu)O-SLTsn_bCM)e6hXLLF(%I}k37#y(-h=+GDd2r zAUY`pMZ;obB*iI;nK81N!gn$9q`C9V_Q*3w`YlGD{V~#NF}6NqF?OXOM&?iyQdFEQ z$0-6|!hQvEZYo~$J@PD&sV^bV;K?%hB}5-YwAb^KWgUgj%Mj})%cYmFJz+0ndycvD zEb_>cEb=n){4`nOUPjRZ7Ogx*4paCqfe4%;m6kxnK)41@G264)BhO2+YYFnqo+4?C zb}mKH%qeo7B81}f6lt^+VhMz+EUz8mwbUceGC8vpc^;lB`x#w#8H)CpDrJ^IL{dzf zDxnnHA^g+ac~*GjSqW0wtEO2EZg5SlB>|xrpgA^G&Mw1Y2@$+!SEovSfmlLOXPQI_ z5}y)mUFQn91}OI%H7n6>iBeYbzzWJ!b7$TM_?R9cOqv8z!u zX@>M&jiTABk#yG#NuzM4AMf=7}jSPrbika~;nBpLWE6ttfm`9#mDZ3VV z-i?<_jPAV_MZbxcptTUG6r~d+j-mj<^>Bi2j{dIr?fZ2?8mvQ}-U(7^9in5_p=fM^ z^rgtASeqbe6wdX?^PxM>#~#~rN;Su%wpkD}Ks*-ue-1M;BhRNYY6J3YnkAtd5WOAIULVeqWfVmezt57h8}JT02^5o0<*OBM@=Oy`dL|QhbsqaTEm*u50c*KY8T2EDg3Gk88G6+JfkqEhyS@w)CaQrWiU~ z(kPr+$TNSo*`8lL@?4RQS;+I|Y-yB*=p~5u`h2!bp~$1~nj`xu>TX4zhvt|(*F5t4 zE~B<0Pp>%=x)sse5$!c&jx3`nqR5ydXDNc;M4p50Jb!xRDU!KwBG0)w67?pcPa)dN zIahX4_-=!!J6CQ{L_oOO@gfIae|zL{$og%_GkmTjZ$tDI7EPKfrzu)&huAq+YHx=~ zgmC4#^OW`~S^qfY$adr@HBUA(y4nsDZ8T4=QiM^2&XZO}r`$N3Vp3q><_A=dDhOCO1qIKW;gO2nlF8Kqv*li z$Wu69(kOiPAkSUAu!GlK9(k%s$34jN!~$uw2hpjB_8PuGrce}6BrTBr6#j1^&klE< znjU%XmQimZ&)EeM`WB+I5$#oWp)7j~8&~ueHZEYHoTUifi}$O?LbE+Ck37CIcQ5k1 zuu!7*BKj1fy)qZdP72>_h_4pP4G5omO$Nu5cB4Mj7MANSUSY!PK98h+vOXJ0+a^hJ zHu79yo^eTXnxe%%h*e2admlt1gzH^*o(DYg)RiOqkmq8OY-V(|{U};xkzAz+qiC^6 zTJ49(fN+gnWVWY)N1ldqVL$T7B00|Jz_(HKz#^&fHbgANrA0EB;vj^}m-orx^{_`C ze<_=TJe`u|5~F+Ppy+eS5|jgxO7Uv4#8DJLxQ@E>JnE4rKpGrCp3BKn=>Vc*4xp&- zi_(`Oo1)c=l1AZt2YJT5Xtt-ZN1j0G_zvU`6j?@5L@_Z%&Qb&)M4r_tW_zCW$kRgR9z>oa zDH3%M(WemYbtOf1QurQ%sI^#bP((nu+AlVFT6^SaBkK<#PxN9*K7{BiEGmoTG)0TU z5N|J*+J_+$AzbI(dD?m836>*=k*Cs2vYFAmt-)-K?qk}P71@Tt4E#?Df=Gs zguE=57~T6l6rJ?41igpt$#@Ujv+8Audk;$ru*CJQJ5P6yJUyhr`^Z!HvQ&B>(J}8M z&s|HTFGV(l6z%1xTY*K=dg@d;PRjc7A~Ese2sTQ*)Wz;F5^r$kTe6*`6UDdBSD=apW1b zOp=eIXc3DpT_&d~fqkT`HXg86oA7T^2 zPGI9^h_pI^C7D_^D6YPnoubl68IdThA_eFTv~@#k`hqbP)MJ-ou?8SjxNS{i(eJOfuq zrH>IE`!R|xTp@iw#!h8_jGfxMLeeOlr*M!jy7Nr($P*(SPa)4eE2YsXahiIushk*C`#i8_s~KXn?rGHsRYr0_ihv1XOrpooBQ9d_rzy~ZW_PzkdB z4D$TAN|MhY`U<0~r<%3$dFi+JOfX>dDWaRFO6^Y(o%ktMG&og8Qsh!3rpjiDYM&v` z8}2-OO#1D4Fkti<@_d{stv*9^2BN)wO_ezmg%s6S%W;ap&ylC|YO_6jI{NK-FktjK z^2}T~K}36PT`lVlbODP# zlO~71z#9>mhj%45O)BL<#6q~z+5up6kSA~dF#yf@G0fD=Xpn( zevdqv>!kMgD4O^^ihi_CMpEQb{Ju^$Q&jr_`*q)XlZOu{zdg@k>GuQjv|TT)en4~v zqP>Q%mpMOR6R!M#gT8RR9Osh2Lgd-z&ckPt-=61vnOcZEr`F5hLPQ@#v{&g3vW~*% zM~EgHD01Yw~6}OyPeGV&i71bPXZ~ z!u6#)&(9utevw_*kf-bxNn^D0cN7iUBIhYWD4yLSjedt%0^v&AVz%cuk33i9%UBptd{ zlK;e#A})D(tDL3?E`m6;RcaR@EU^e-*S5;YA}l$@B@etQn<;$%g6RIHTxD3;UkID= zrnLGCA_GD~E~9^v^cVUkg1N^pue%&jK8tkgWDwTZ!9U`lJB?4VTh&vWiW4b`_f8f>MxBg*;YPI1Y&owAd{*9oy| zr`%vzq!VH9@01QsEXm}O>pLZx;tIusyW})Q3vY-XyQH=^L?T7pE*VLYOR;H}Y^JDI z2I8Y#I5{A~DE{6ht;#@TP~5*;=1>$;^xQ4SDFVwv#O;9-_}!lwd6gSX^7!$K+`tm|9Sr~CgQ?#fA(LP&hSAs~Scs5%`Qsh!B z&X&y-)ha_A%$BPZVHCe-ORLHd85DK)$(+jA)GL*-sqOd4aV`n0f+b`2NsTHHu@J8L z`%DeerL_45iWy$FbGK*6y2{&C$~;nSy*r>Q0!m{NIs$sjZkDfIwM>n`eeT5}x|roY zb`$#eP8&XY?4+PT3P(4_c#Xc--sfA^>)gIwNma{CYG1xXP*A(}PX#p{B45`oUG14R zqa8zrJQna+v&Ml>wH-2i{KOH%C&mn!Iw>Y%%y?baHaMtl^T(cQI^-s59MrU3yP)QQ zHhkQOal@mh51FI|g4;C@YTCGcV>!^jO!fG-k2i1IG`Pc4C_Zsa)F_Q;`gq{u?HV_0 zK4f^z^ym?ang_IftV7eLfkTE3oirk5QiNG`hbDoK1q3u3GIdN;RE(m4;DCU~nxbg* z#PP#NOqvukdg6$o5tD{SkI}^)nm2vC@#9T`hhU4wPn09bHsJ;jjT|#7$|Mfz(7bVnW^IRziX0lzX1GJv SJX^-KtK!fyzfLJt;eP=(3%8H} diff --git a/OpenGL-Core/vendor/Glad/bin/Debug-windows-x86_64/Glad/Glad.pdb b/OpenGL-Core/vendor/Glad/bin/Debug-windows-x86_64/Glad/Glad.pdb index e1ec1f1b665f0531835f5eb59618476e9006f682..a6dd24c48580e4e79957bcc716f2047451706ea8 100644 GIT binary patch delta 4020 zcmb_feNY?66@Pn&K!^_sJH}w32u#x?H4I>xB(^6Ygs`ucbc!TH@TenXaW-m%pwh|4 z{UgHDN$VsY*A1DubUJbEBzDuJ`M{)5PveY}bkf?%BzEEsiEEc++O&25aKLH4JE3py z_Jja;+D@XGr@i-nzxVd-+r8brk#}-O4(A=bp{NP~`cdewVw?=UV$PX?{+aO-%lqb0 ztEGsCQ$Ns^;^U1*E8)lUy`P&&uVNS>)rKc1isU*xU#sYhWq$W&^+DVpAdn%EXm)hMjb!cV)m*wCK z4@LW~VW<4H%+6==y$MNr_^c&8FeFEJNm^prIQ6qJznn#Wnvb|+M^cjE11)(&7Ki$} z!^3bJU*Xf6JL#w^pwiIYC^1$5aRPPfw&2>AmP(nj2-Vjj`ww!1<;rz61ZBdXmkb~!@flujq_l{9bn2qpztLgQm+BH>v1QhZ#@3SOYqMM za39_4+5q0bTGaK9*$)&xN-Vb)oM|GrAVArFK;)e;KrYP9VYc|MZ3Y4Kq+&&ztnHX^ z(`Zdo>A2W9X0C{Das|ENgdB~;B!6@u`&s#J0jX4eTtMooW>*1UYJeH+XBOo}lz*Xo zj&gN&a@a~9v=X24kdm#k!U`8l*!Vlu^$TY4Wp zQ`UA4z@z$0s((XcFf|a|9gWB3hJKi()QdrP4%lxe>bA(|77|!(?4SR1k*oZ z`V{J2*{Ob7L#opk5d933!i};N8=&W+y|2#~-VHc*%|_2aAs$Ic15#Y32(J55hS4KR zzA;$7-G2 z3#TNNloB{8I7m7T=aK$bq?=j+^qlGOi8=cvc~Q#fqs_8W5BUJOE`1r@%>`QTM_BKN zJ2m|grdzja`cbA|v;lk%4C-7t)(5v@PRZb%;Y2JE6ys7y0?&kC$6zEyrz#l~hotz< znA$8|?nZs>EAR58K12ug81`}nv!d_Tj<2SdOeaSSq?{y_lLoRs+d%! zZMKHB>zW&H4sMUegPRAF@*OR<;4q5A)gTQ^wBR4HTIb1~YzOrzu3P8^@8mAl8>LP2sG%QPrcsRDc zd#&qX65HbQkUUeF%qZpBNHh#>1#I?QJyhn%_1@? zR&0$&<$#owgVA^duCn=3-Y+Ir#cCpXY6P|$&gGLEvh}CUq@6*Jn@AO%hA;F0drhQ{ zVKXMuy5^RKrXcPsu~eUgJ7sOwu!UM&7~aI-&O~^?ihGNc%nX4pvs?8F&;+UbfAY;* z1lI_&E)ym!#|Cyp{#&#~A+S_m&(F-1LfS}+ZN97txfU`j@P8W`t-UgqPnIu=TAGQv zph#+uE*(quhZ9mC_IW+zl+tTBJzG9Np=Zj6DD*7(7=@l8pHdyCk;wf z!a*#CD|I+BG;~eI^~z8iDcH1Ra_@ixY$5zVFa0quu}*l&Mn1ja8w5)6%w0Xf!76O; zNBPb%fWKk;Hp(>0C6qs-)E?zPz)w$0gn)mN!0k@}{BV?q^>Z9Ni8qzuF#_B7^Y9pc zUcOD`01wmHM%M7KFAuLj_`DzGUX+6<7tin;VH6)K*W*3vEgswg4^wzYdhZH>dqfU4 z{))iz_jtH`CxNQH1PYE4m_EV7nK)u~+~fv7$uGQrrm+4Ctaw`^K;EJo&L=$13<|xv!UunAvCXjlF2!f>O2}|L`}AvC6Tfw<79?pGK-)GFsoevZfd{%vz0# e#{u39=ocaY-b%rYvU1G+Q!`mLNe+^^>VE@W9_&>B delta 273 zcmZozz~1nHZGr}`Gy?-e0uY0MyWZeXDQJ^4$01yKu z7TL7a7GZTZrM#on? zAPLo@KojNwtzl>Q|DOeD!2kas;}{r3fD{OG0NFCz4HFp8^KUa?6abq4fp1v=qr$Wa hj2uvBGsaA2O^|101F96?teJ3~pHX5v;|9k6@&IQLJWT)q diff --git a/OpenGL-Sandbox/Shaders/fragment.frag.glsl b/OpenGL-Sandbox/Shaders/fragment.frag.glsl new file mode 100644 index 00000000..eeabd77b --- /dev/null +++ b/OpenGL-Sandbox/Shaders/fragment.frag.glsl @@ -0,0 +1,14 @@ +#version 330 core + +out vec4 FragColor; + +in vec3 ourColor; +in vec2 TexCoord; + +// texture sampler +uniform sampler2D texture1; + +void main() +{ + FragColor = texture(texture1, TexCoord); +} \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/vertex.vert.glsl b/OpenGL-Sandbox/Shaders/vertex.vert.glsl new file mode 100644 index 00000000..9a932428 --- /dev/null +++ b/OpenGL-Sandbox/Shaders/vertex.vert.glsl @@ -0,0 +1,15 @@ +#version 330 core + +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aColor; +layout (location = 2) in vec2 aTexCoord; + +out vec3 ourColor; +out vec2 TexCoord; + +void main() +{ + gl_Position = vec4(aPos, 1.0); + ourColor = aColor; + TexCoord = vec2(aTexCoord.x, aTexCoord.y); +} \ No newline at end of file diff --git a/OpenGL-Sandbox/assets/container.jpg b/OpenGL-Sandbox/assets/container.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1c5f7d52bbc0fad7097167cf5aa2c24cff76cee3 GIT binary patch literal 25769 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%0AWEC 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 BJ>LKT literal 0 HcmV?d00001 diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index 1b1c4f30..3e3841a4 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -1,4 +1,6 @@ #include "SandboxLayer.h" +#include "GLCore/Util/Shader.h" + using namespace GLCore; using namespace GLCore::Utils; @@ -15,7 +17,48 @@ void SandboxLayer::OnAttach() { EnableGLDebugging(); - // Init here + m_shader = Shader::FromGLSLTextFiles("Shaders/vertex.vert.glsl", "Shaders/fragment.frag.glsl"); + + + float vertices[] = { + // positions // colors // texture coords + 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right + 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right + -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left + -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left + }; + + unsigned int indices[] = { + 0, 1, 3, // first triangle + 1, 2, 3 // second triangle + }; + + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(VAO); + + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // position attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + // color attribute + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); + glEnableVertexAttribArray(1); + // texture coord attribute + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); + glEnableVertexAttribArray(2); + + m_texture = LoadTexture("assets/container.jpg"); + + + } void SandboxLayer::OnDetach() @@ -30,7 +73,21 @@ void SandboxLayer::OnEvent(Event& event) void SandboxLayer::OnUpdate(Timestep ts) { - // Render here + glClearColor(0.2f, 0.3f, 0.3f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + glBindTexture(GL_TEXTURE_2D, m_texture.TextureID); + + glUseProgram(m_shader->GetRendererID()); + + glBindVertexArray(VAO); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + + + + + + } void SandboxLayer::OnImGuiRender() diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index f2096283..d885df2a 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -2,6 +2,8 @@ #include #include +#include "GLCore/Util/Texture.h" +#include "GLCore/Util/Shader.h" class SandboxLayer : public GLCore::Layer { @@ -15,4 +17,7 @@ class SandboxLayer : public GLCore::Layer virtual void OnUpdate(GLCore::Timestep ts) override; virtual void OnImGuiRender() override; private: + Texture m_texture; + GLCore::Utils::Shader* m_shader; + unsigned int VBO, VAO, EBO; }; \ No newline at end of file From e5c5c0245f2a8e6c777ce1dffee87100f561f1c2 Mon Sep 17 00:00:00 2001 From: karl2805 Date: Fri, 19 Dec 2025 01:54:46 +0000 Subject: [PATCH 02/11] Compute Shader Working --- OpenGL-Core/src/GLCore/Util/Renderer.cpp | 95 +++++++++++++++++++++++ OpenGL-Core/src/GLCore/Util/Renderer.h | 25 ++++++ OpenGL-Core/src/GLCore/Util/Shader.cpp | 25 +++++- OpenGL-Core/src/GLCore/Util/Shader.h | 13 +++- OpenGL-Sandbox/Shaders/compute.comp.glsl | 25 ++++++ OpenGL-Sandbox/Shaders/fragment.frag.glsl | 2 +- OpenGL-Sandbox/Shaders/vertex.vert.glsl | 2 +- OpenGL-Sandbox/src/SandboxLayer.cpp | 72 ++++++++++++++++- OpenGL-Sandbox/src/SandboxLayer.h | 18 ++++- 9 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 OpenGL-Core/src/GLCore/Util/Renderer.cpp create mode 100644 OpenGL-Core/src/GLCore/Util/Renderer.h create mode 100644 OpenGL-Sandbox/Shaders/compute.comp.glsl diff --git a/OpenGL-Core/src/GLCore/Util/Renderer.cpp b/OpenGL-Core/src/GLCore/Util/Renderer.cpp new file mode 100644 index 00000000..211c2884 --- /dev/null +++ b/OpenGL-Core/src/GLCore/Util/Renderer.cpp @@ -0,0 +1,95 @@ +#include "glpch.h" +#include "Renderer.h" + +#include + +#include "stb_image.h" + + + +Texture CreateTexture(int width, int height) +{ + Texture result; + result.Width = width; + result.Height = height; + + glCreateTextures(GL_TEXTURE_2D, 1, &result.Handle); + + glTextureStorage2D(result.Handle, 1, GL_RGBA32F, width, height); + + glTextureParameteri(result.Handle, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(result.Handle, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glTextureParameteri(result.Handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTextureParameteri(result.Handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + return result; +} + +Texture LoadTexture(const std::filesystem::path& path) +{ + int width, height, channels; + std::string filepath = path.string(); + unsigned char* data = stbi_load(filepath.c_str(), &width, &height, &channels, 0); + + if (!data) + { + std::cerr << "Failed to load texture: " << filepath << "\n"; + return {}; + } + + GLenum format = channels == 4 ? GL_RGBA : + channels == 3 ? GL_RGB : + channels == 1 ? GL_RED : 0; + + Texture result; + result.Width = width; + result.Height = height; + + glCreateTextures(GL_TEXTURE_2D, 1, &result.Handle); + + glTextureStorage2D(result.Handle, 1, (format == GL_RGBA ? GL_RGBA8 : GL_RGB8), width, height); + + glTextureSubImage2D(result.Handle, 0, 0, 0, width, height, format, GL_UNSIGNED_BYTE, data); + + glTextureParameteri(result.Handle, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTextureParameteri(result.Handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTextureParameteri(result.Handle, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTextureParameteri(result.Handle, GL_TEXTURE_WRAP_T, GL_REPEAT); + + glGenerateTextureMipmap(result.Handle); + stbi_image_free(data); + + return result; +} + +Framebuffer CreateFramebufferWithTexture(const Texture texture) +{ + Framebuffer result; + + glCreateFramebuffers(1, &result.Handle); + + if (!AttachTextureToFramebuffer(result, texture)) + { + glDeleteFramebuffers(1, &result.Handle); + return {}; + } + + return result; +} + +bool AttachTextureToFramebuffer(Framebuffer& framebuffer, const Texture texture) +{ + glNamedFramebufferTexture(framebuffer.Handle, GL_COLOR_ATTACHMENT0, texture.Handle, 0); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + std::cerr << "Framebuffer is not complete!" << std::endl; + return false; + } + + framebuffer.ColorAttachment = texture; + return true; +} + diff --git a/OpenGL-Core/src/GLCore/Util/Renderer.h b/OpenGL-Core/src/GLCore/Util/Renderer.h new file mode 100644 index 00000000..604e323f --- /dev/null +++ b/OpenGL-Core/src/GLCore/Util/Renderer.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#define GLFW_INCLUDE_NONE +#include + +#include + +struct Texture +{ + GLuint Handle = 0; + uint32_t Width = 0; + uint32_t Height = 0; +}; + +struct Framebuffer +{ + GLuint Handle = 0; + Texture ColorAttachment; +}; + +Texture CreateTexture(int width, int height); +Texture LoadTexture(const std::filesystem::path& path); +Framebuffer CreateFramebufferWithTexture(const Texture texture); +bool AttachTextureToFramebuffer(Framebuffer& framebuffer, const Texture texture); diff --git a/OpenGL-Core/src/GLCore/Util/Shader.cpp b/OpenGL-Core/src/GLCore/Util/Shader.cpp index 075400ec..7e65822b 100644 --- a/OpenGL-Core/src/GLCore/Util/Shader.cpp +++ b/OpenGL-Core/src/GLCore/Util/Shader.cpp @@ -107,7 +107,7 @@ namespace GLCore::Utils { m_RendererID = program; } - void Shader::CreateComputeShader(const std::string& filepath) + GLint CreateComputeShader(const std::string& filepath) { std::string shaderSource = ReadFileAsString(filepath); @@ -131,7 +131,7 @@ namespace GLCore::Utils { std::cerr << infoLog.data() << std::endl; glDeleteShader(shaderHandle); - return; + return 0; } GLuint program = glCreateProgram(); @@ -153,12 +153,29 @@ namespace GLCore::Utils { glDeleteProgram(program); glDeleteShader(shaderHandle); - return; + return 0; } glDetachShader(program, shaderHandle); - m_ComputeID = program; + return program; } + GLint ReloadComputeShader(GLint ComputeID, const std::string& filepath) + { + glDeleteProgram(ComputeID); + + return CreateComputeShader(filepath); + } + + Shader* ReloadGraphicsShader(Shader* shader, const std::string& vertexShaderPath, const std::string& fragmentShaderPath) + { + delete shader; + Shader* newShader = Shader::FromGLSLTextFiles(vertexShaderPath, fragmentShaderPath); + + return newShader; + } + + + } \ No newline at end of file diff --git a/OpenGL-Core/src/GLCore/Util/Shader.h b/OpenGL-Core/src/GLCore/Util/Shader.h index ff027a38..e578a546 100644 --- a/OpenGL-Core/src/GLCore/Util/Shader.h +++ b/OpenGL-Core/src/GLCore/Util/Shader.h @@ -8,21 +8,30 @@ namespace GLCore::Utils { class Shader { + public: + ~Shader(); GLuint GetRendererID() { return m_RendererID; } static Shader* FromGLSLTextFiles(const std::string& vertexShaderPath, const std::string& fragmentShaderPath); + private: Shader() = default; void LoadFromGLSLTextFiles(const std::string& vertexShaderPath, const std::string& fragmentShaderPath); GLuint CompileShader(GLenum type, const std::string& source); - void CreateComputeShader(const std::string& filepath); + private: GLuint m_RendererID; - GLuint m_ComputeID; + }; + + GLint CreateComputeShader(const std::string& filepath); + GLint ReloadComputeShader(GLint ComputeID, std::string& filepath); + Shader* ReloadGraphicsShader(Shader* shader, const std::string& vertexShaderPath, const std::string& fragmentShaderPath); + + } \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/compute.comp.glsl b/OpenGL-Sandbox/Shaders/compute.comp.glsl new file mode 100644 index 00000000..baaf5a39 --- /dev/null +++ b/OpenGL-Sandbox/Shaders/compute.comp.glsl @@ -0,0 +1,25 @@ +#version 430 + +#define MAX_ITERATION 500 + +layout(local_size_x = 1, local_size_y = 1) in; +layout(rgba32f, binding = 0) uniform image2D img_output; + +uniform mat4 translate; + +vec2 resolution = vec2(1280, 720); + +void main() { + vec4 pixel = vec4(0.5, 0.5, 0.5, 1.0); + ivec2 pixel_coord = ivec2(gl_GlobalInvocationID.xy); + + + + + + + + + // output to a specific pixel in the image + imageStore(img_output, pixel_coord, pixel); +} \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/fragment.frag.glsl b/OpenGL-Sandbox/Shaders/fragment.frag.glsl index eeabd77b..95c9893b 100644 --- a/OpenGL-Sandbox/Shaders/fragment.frag.glsl +++ b/OpenGL-Sandbox/Shaders/fragment.frag.glsl @@ -1,4 +1,4 @@ -#version 330 core +#version 430 core out vec4 FragColor; diff --git a/OpenGL-Sandbox/Shaders/vertex.vert.glsl b/OpenGL-Sandbox/Shaders/vertex.vert.glsl index 9a932428..faafb83a 100644 --- a/OpenGL-Sandbox/Shaders/vertex.vert.glsl +++ b/OpenGL-Sandbox/Shaders/vertex.vert.glsl @@ -1,4 +1,4 @@ -#version 330 core +#version 430 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aColor; diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index 3e3841a4..bf178eb5 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -1,5 +1,6 @@ #include "SandboxLayer.h" #include "GLCore/Util/Shader.h" +#include using namespace GLCore; @@ -18,6 +19,9 @@ void SandboxLayer::OnAttach() EnableGLDebugging(); m_shader = Shader::FromGLSLTextFiles("Shaders/vertex.vert.glsl", "Shaders/fragment.frag.glsl"); + + m_comp_shader = CreateComputeShader("Shaders/compute.comp.glsl"); + float vertices[] = { @@ -57,7 +61,51 @@ void SandboxLayer::OnAttach() m_texture = LoadTexture("assets/container.jpg"); + // dimensions of the image + + + glGenTextures(1, &tex_output); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex_output); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, tex_w, tex_h, 0, GL_RGBA, GL_FLOAT, NULL); + glBindImageTexture(0, tex_output, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F); + + int work_grp_cnt[3]; + + glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0, &work_grp_cnt[0]); + glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 1, &work_grp_cnt[1]); + glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 2, &work_grp_cnt[2]); + + printf("max global (total) work group counts x:%i y:%i z:%i\n", work_grp_cnt[0], work_grp_cnt[1], work_grp_cnt[2]); + + int work_grp_size[3]; + + glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 0, &work_grp_size[0]); + glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 1, &work_grp_size[1]); + glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 2, &work_grp_size[2]); + + printf("max local (in one shader) work group sizes x:%i y:%i z:%i\n", + work_grp_size[0], work_grp_size[1], work_grp_size[2]); + + int work_grp_inv; + + glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &work_grp_inv); + printf("max local work group invocations %i\n", work_grp_inv); + + glm::mat4 transform = glm::mat4(0.0f); + + glm::vec3 pos = glm::vec3(1280.0f/2, 720.0f/2, 0.0f); + + transform = glm::translate(transform, pos); + int vertexColorLocation = glGetUniformLocation(m_shader->GetRendererID(), "translate"); + glUseProgram(m_shader->GetRendererID()); + glUniformMatrix4fv(vertexColorLocation, 1, GL_FALSE, &transform[0][0]); + } @@ -68,22 +116,38 @@ void SandboxLayer::OnDetach() void SandboxLayer::OnEvent(Event& event) { - // Events here + EventDispatcher dispatcher(event); + dispatcher.Dispatch([&]{ + + + }); + + + + } void SandboxLayer::OnUpdate(Timestep ts) { - glClearColor(0.2f, 0.3f, 0.3f, 1.0f); + + glUseProgram(m_comp_shader); + glDispatchCompute((GLuint)tex_w, (GLuint)tex_h, 1); + + // make sure writing to image has finished before read + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + + + glClearColor(0.5f, 0.5f, 0.5f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glBindTexture(GL_TEXTURE_2D, m_texture.TextureID); glUseProgram(m_shader->GetRendererID()); - glBindVertexArray(VAO); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex_output); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); - diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index d885df2a..1e0aa211 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -18,6 +18,22 @@ class SandboxLayer : public GLCore::Layer virtual void OnImGuiRender() override; private: Texture m_texture; - GLCore::Utils::Shader* m_shader; unsigned int VBO, VAO, EBO; + GLCore::Utils::Shader* m_shader; + + + GLint m_comp_shader; + + int m_WindowWidth, m_WindowHeight; + + GLuint tex_output; + + + glm::vec2 mouse_pos; + + + + + + }; \ No newline at end of file From b8d6136a1ec8254850faf5d02d07760c02154a48 Mon Sep 17 00:00:00 2001 From: karl2805 Date: Tue, 23 Dec 2025 01:10:23 +0000 Subject: [PATCH 03/11] game of life nearly --- OpenGL-Core/src/GLCore/Core/Application.cpp | 3 + OpenGL-Core/src/GLCore/Core/Layer.h | 2 + OpenGL-Sandbox/Shaders/compute.comp.glsl | 59 ++++-- OpenGL-Sandbox/Shaders/fragment.frag.glsl | 29 ++- OpenGL-Sandbox/Shaders/vertex.vert.glsl | 39 +++- OpenGL-Sandbox/src/SandboxApp.cpp | 1 + OpenGL-Sandbox/src/SandboxLayer.cpp | 187 +++++++++++--------- OpenGL-Sandbox/src/SandboxLayer.h | 19 +- 8 files changed, 229 insertions(+), 110 deletions(-) diff --git a/OpenGL-Core/src/GLCore/Core/Application.cpp b/OpenGL-Core/src/GLCore/Core/Application.cpp index ad93a482..8c81c184 100644 --- a/OpenGL-Core/src/GLCore/Core/Application.cpp +++ b/OpenGL-Core/src/GLCore/Core/Application.cpp @@ -65,7 +65,10 @@ namespace GLCore { m_LastFrameTime = time; for (Layer* layer : m_LayerStack) + { layer->OnUpdate(timestep); + layer->OnRender(); + } m_ImGuiLayer->Begin(); for (Layer* layer : m_LayerStack) diff --git a/OpenGL-Core/src/GLCore/Core/Layer.h b/OpenGL-Core/src/GLCore/Core/Layer.h index 8e4bf99a..a930ec27 100644 --- a/OpenGL-Core/src/GLCore/Core/Layer.h +++ b/OpenGL-Core/src/GLCore/Core/Layer.h @@ -15,6 +15,8 @@ namespace GLCore { virtual void OnAttach() {} virtual void OnDetach() {} virtual void OnUpdate(Timestep ts) {} + virtual void OnRender() {} + virtual void OnImGuiRender() {} virtual void OnEvent(Event& event) {} diff --git a/OpenGL-Sandbox/Shaders/compute.comp.glsl b/OpenGL-Sandbox/Shaders/compute.comp.glsl index baaf5a39..06523ec2 100644 --- a/OpenGL-Sandbox/Shaders/compute.comp.glsl +++ b/OpenGL-Sandbox/Shaders/compute.comp.glsl @@ -1,25 +1,60 @@ #version 430 -#define MAX_ITERATION 500 +layout(local_size_x = 8, local_size_y = 8) in; -layout(local_size_x = 1, local_size_y = 1) in; -layout(rgba32f, binding = 0) uniform image2D img_output; +layout(binding = 1, std430) readonly buffer ssbo_in +{ + int State_in[]; +}; -uniform mat4 translate; +layout(binding = 2, std430) buffer ssbo_out +{ + int State_out[]; +}; -vec2 resolution = vec2(1280, 720); - -void main() { - vec4 pixel = vec4(0.5, 0.5, 0.5, 1.0); - ivec2 pixel_coord = ivec2(gl_GlobalInvocationID.xy); +uniform int u_GridSize; +int CellIndex(vec2 CellCoord) +{ + return int(CellCoord.y) * u_GridSize + int(CellCoord.x); +} +int cellActive(int x, int y) +{ + return int(State_in[CellIndex(vec2(x,y))]); +} +void main() { + vec2 cell = gl_GlobalInvocationID.xy; + + int activeNeighbors = cellActive(int(cell.x+1), int(cell.y+1)) + + cellActive(int(cell.x+1), int(cell.y)) + + cellActive(int(cell.x+1), int(cell.y-1)) + + cellActive(int(cell.x), int(cell.y-1)) + + cellActive(int(cell.x-1), int(cell.y-1)) + + cellActive(int(cell.x-1), int(cell.y)) + + cellActive(int(cell.x-1), int(cell.y+1)) + + cellActive(int(cell.x), int(cell.y+1)); + +int i = CellIndex(cell.xy); + +switch (activeNeighbors) { + case 2: { // Active cells with 2 neighbors stay active. + State_out[i] = State_in[i]; + break; + } + case 3: { // Cells with 3 neighbors become or stay active. + State_out[i] = 1; + break; + } + default: { // Cells with < 2 or > 3 neighbors become inactive. + State_out[i] = 0; + } + + + } - - // output to a specific pixel in the image - imageStore(img_output, pixel_coord, pixel); } \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/fragment.frag.glsl b/OpenGL-Sandbox/Shaders/fragment.frag.glsl index 95c9893b..6f9a53ce 100644 --- a/OpenGL-Sandbox/Shaders/fragment.frag.glsl +++ b/OpenGL-Sandbox/Shaders/fragment.frag.glsl @@ -1,14 +1,29 @@ -#version 430 core +#version 440 core + + + + +in vec2 v_LocalPosition; -out vec4 FragColor; -in vec3 ourColor; -in vec2 TexCoord; -// texture sampler -uniform sampler2D texture1; + +out vec4 FragColor; + +float BorderWidth = 0.0f; void main() { - FragColor = texture(texture1, TexCoord); + + + FragColor = vec4(0.08, 0.33, 0.78, 1.0); + + //set border + if (v_LocalPosition.x < -1 + BorderWidth || v_LocalPosition.x > 1 - BorderWidth || + v_LocalPosition.y < -1 + BorderWidth || v_LocalPosition.y > 1 - BorderWidth) + { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); + } + + } \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/vertex.vert.glsl b/OpenGL-Sandbox/Shaders/vertex.vert.glsl index faafb83a..47f6546c 100644 --- a/OpenGL-Sandbox/Shaders/vertex.vert.glsl +++ b/OpenGL-Sandbox/Shaders/vertex.vert.glsl @@ -1,15 +1,38 @@ #version 430 core -layout (location = 0) in vec3 aPos; -layout (location = 1) in vec3 aColor; -layout (location = 2) in vec2 aTexCoord; -out vec3 ourColor; -out vec2 TexCoord; +layout (location = 0) in vec2 aPos; + +layout(std430, binding = 1) buffer layoutName +{ + int data_SSBO[]; +}; + + +out vec2 v_LocalPosition; +uniform int u_GridSize; + +float temp = 0.9f; + +int i = int(gl_InstanceID); + +vec2 cell = vec2(i % u_GridSize, floor(i / u_GridSize)); + +int State = data_SSBO[gl_InstanceID]; + + + void main() { - gl_Position = vec4(aPos, 1.0); - ourColor = aColor; - TexCoord = vec2(aTexCoord.x, aTexCoord.y); + + v_LocalPosition = aPos; + vec2 position = (aPos + 1) / u_GridSize - 1; + + position = position * State + 2 * (cell / u_GridSize); + + + + gl_Position = vec4(position, 0.0f, 1.0f); + } \ No newline at end of file diff --git a/OpenGL-Sandbox/src/SandboxApp.cpp b/OpenGL-Sandbox/src/SandboxApp.cpp index 8f527132..e84829bc 100644 --- a/OpenGL-Sandbox/src/SandboxApp.cpp +++ b/OpenGL-Sandbox/src/SandboxApp.cpp @@ -7,6 +7,7 @@ class Sandbox : public Application { public: Sandbox() + :Application("Phase Change Simulation", 1000, 1000) { PushLayer(new SandboxLayer()); } diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index bf178eb5..a78eb88b 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -1,13 +1,30 @@ #include "SandboxLayer.h" #include "GLCore/Util/Shader.h" #include - +#include +#include "GLCore/Core/KeyCodes.h" using namespace GLCore; using namespace GLCore::Utils; SandboxLayer::SandboxLayer() { + for (int i = 0; i < GRID_SIZE * GRID_SIZE; i++) + { + int choices = 10; + int picked_choice = (int)(rand() % choices); + if (picked_choice == 1) { + m_ParticleStates_in[i] = 1; + } + } + + /* m_ParticleStates_in[500] = 1; + m_ParticleStates_in[500] = 1; + m_ParticleStates_in[500] = 1; + m_ParticleStates_in[500] = 1; + m_ParticleStates_in[500] = 1;*/ + + } SandboxLayer::~SandboxLayer() @@ -23,13 +40,12 @@ void SandboxLayer::OnAttach() m_comp_shader = CreateComputeShader("Shaders/compute.comp.glsl"); - float vertices[] = { - // positions // colors // texture coords - 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right - 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right - -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left - -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left + // positions + 1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 0.0f, + -1.0f, -1.0f, 0.0f, + -1.0f, 1.0f, 0.0f }; unsigned int indices[] = { @@ -37,76 +53,36 @@ void SandboxLayer::OnAttach() 1, 2, 3 // second triangle }; - glGenVertexArrays(1, &VAO); - glGenBuffers(1, &VBO); - glGenBuffers(1, &EBO); + glGenVertexArrays(1, &m_VAO); + glGenBuffers(1, &m_VBO); + glGenBuffers(1, &m_EBO); - glBindVertexArray(VAO); + glBindVertexArray(m_VAO); - glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBindBuffer(GL_ARRAY_BUFFER, m_VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); - // color attribute - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - // texture coord attribute - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); - glEnableVertexAttribArray(2); - m_texture = LoadTexture("assets/container.jpg"); - - // dimensions of the image - - glGenTextures(1, &tex_output); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, tex_output); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, tex_w, tex_h, 0, GL_RGBA, GL_FLOAT, NULL); - glBindImageTexture(0, tex_output, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F); - - int work_grp_cnt[3]; - - glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0, &work_grp_cnt[0]); - glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 1, &work_grp_cnt[1]); - glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 2, &work_grp_cnt[2]); - - printf("max global (total) work group counts x:%i y:%i z:%i\n", work_grp_cnt[0], work_grp_cnt[1], work_grp_cnt[2]); - - int work_grp_size[3]; - - glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 0, &work_grp_size[0]); - glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 1, &work_grp_size[1]); - glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 2, &work_grp_size[2]); - - printf("max local (in one shader) work group sizes x:%i y:%i z:%i\n", - work_grp_size[0], work_grp_size[1], work_grp_size[2]); - - int work_grp_inv; - - glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &work_grp_inv); - printf("max local work group invocations %i\n", work_grp_inv); + //create SSBO Buffer + glGenBuffers(1, &m_SSBO_in); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_in), m_ParticleStates_in, GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + glGenBuffers(1, &m_SSBO_out); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_out); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_out), m_ParticleStates_out, GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - glm::mat4 transform = glm::mat4(0.0f); - - glm::vec3 pos = glm::vec3(1280.0f/2, 720.0f/2, 0.0f); - - transform = glm::translate(transform, pos); - - int vertexColorLocation = glGetUniformLocation(m_shader->GetRendererID(), "translate"); - glUseProgram(m_shader->GetRendererID()); - glUniformMatrix4fv(vertexColorLocation, 1, GL_FALSE, &transform[0][0]); - - } void SandboxLayer::OnDetach() @@ -114,43 +90,96 @@ void SandboxLayer::OnDetach() // Shutdown here } +int CellIndex(glm::vec2 CellCoord) +{ + return int(CellCoord.y) * GRID_SIZE + int(CellCoord.x) * 0.001; +} + void SandboxLayer::OnEvent(Event& event) { EventDispatcher dispatcher(event); - dispatcher.Dispatch([&]{ - + dispatcher.Dispatch( + [&](MouseMovedEvent& e) + { + m_MousePos.x = e.GetX(); + m_MousePos.y = e.GetY(); + + std::cout << m_MousePos.x << " " << m_MousePos.y << "\n"; + + return false; }); + dispatcher.Dispatch( + [&](MouseButtonPressedEvent& e) + { + + m_ParticleStates_in[1000] = 1; + m_ParticleStates_out[1000] = 1; + + std::cout << "preessed"; + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_in), m_ParticleStates_in, GL_DYNAMIC_STORAGE_BIT); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + return false; + }); + + dispatcher.Dispatch( + [&](KeyPressedEvent&e) + { + m_Compute = m_Compute ? false : true; + + return false; + }); + + } void SandboxLayer::OnUpdate(Timestep ts) { + - glUseProgram(m_comp_shader); - glDispatchCompute((GLuint)tex_w, (GLuint)tex_h, 1); + glClearColor(0.5f, 0.5f, 0.5f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); - // make sure writing to image has finished before read - glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + + glUseProgram(m_shader->GetRendererID()); + - glClearColor(0.5f, 0.5f, 0.5f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); + //set uniforms - glBindTexture(GL_TEXTURE_2D, m_texture.TextureID); + int location = glGetUniformLocation(m_shader->GetRendererID(), "u_GridSize"); + glUniform1i(location, m_GridSize); - glUseProgram(m_shader->GetRendererID()); + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0, GRID_SIZE * GRID_SIZE); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, tex_output); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + + + + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); + + Sleep(100); + + glUseProgram(m_comp_shader); + + glDispatchCompute(GRID_SIZE / 8, GRID_SIZE / 8, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + std::swap(m_SSBO_in, m_SSBO_out); +} + +void SandboxLayer::OnRender() +{ } diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index 1e0aa211..6ca5ed26 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -1,5 +1,10 @@ #pragma once +#define GRID_SIZE 64 + + + + #include #include #include "GLCore/Util/Texture.h" @@ -15,25 +20,31 @@ class SandboxLayer : public GLCore::Layer virtual void OnDetach() override; virtual void OnEvent(GLCore::Event& event) override; virtual void OnUpdate(GLCore::Timestep ts) override; + virtual void OnRender() override; virtual void OnImGuiRender() override; private: Texture m_texture; - unsigned int VBO, VAO, EBO; + GLuint m_VBO, m_VAO, m_EBO, m_SSBO_in, m_SSBO_out; GLCore::Utils::Shader* m_shader; GLint m_comp_shader; - int m_WindowWidth, m_WindowHeight; + int m_WindowHeight = 720; + int m_WindowWidth = 1280; GLuint tex_output; + glm::vec2 m_MousePos; - glm::vec2 mouse_pos; + int m_GridSize = GRID_SIZE; + int m_ParticleStates_in[GRID_SIZE * GRID_SIZE] = { 0 }; + int m_ParticleStates_out[GRID_SIZE * GRID_SIZE] = { 0 }; + bool m_Compute = false; + int m_Counter = 0; - }; \ No newline at end of file From a148251b99b1edaa473f6d89964bca7e0b27430a Mon Sep 17 00:00:00 2001 From: Karl Riedewald Date: Tue, 23 Dec 2025 02:53:24 +0000 Subject: [PATCH 04/11] game of life working! --- ...compute.comp.glsl => GameOfLife.comp.glsl} | 2 +- OpenGL-Sandbox/Shaders/Swap.comp.glsl | 44 +++++++++++++++++++ OpenGL-Sandbox/src/SandboxLayer.cpp | 39 ++++++---------- OpenGL-Sandbox/src/SandboxLayer.h | 2 +- 4 files changed, 59 insertions(+), 28 deletions(-) rename OpenGL-Sandbox/Shaders/{compute.comp.glsl => GameOfLife.comp.glsl} (97%) create mode 100644 OpenGL-Sandbox/Shaders/Swap.comp.glsl diff --git a/OpenGL-Sandbox/Shaders/compute.comp.glsl b/OpenGL-Sandbox/Shaders/GameOfLife.comp.glsl similarity index 97% rename from OpenGL-Sandbox/Shaders/compute.comp.glsl rename to OpenGL-Sandbox/Shaders/GameOfLife.comp.glsl index 06523ec2..35032e61 100644 --- a/OpenGL-Sandbox/Shaders/compute.comp.glsl +++ b/OpenGL-Sandbox/Shaders/GameOfLife.comp.glsl @@ -38,7 +38,7 @@ void main() { cellActive(int(cell.x-1), int(cell.y+1)) + cellActive(int(cell.x), int(cell.y+1)); -int i = CellIndex(cell.xy); +int i = CellIndex(cell); switch (activeNeighbors) { case 2: { // Active cells with 2 neighbors stay active. diff --git a/OpenGL-Sandbox/Shaders/Swap.comp.glsl b/OpenGL-Sandbox/Shaders/Swap.comp.glsl new file mode 100644 index 00000000..9a683737 --- /dev/null +++ b/OpenGL-Sandbox/Shaders/Swap.comp.glsl @@ -0,0 +1,44 @@ +#version 430 + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(binding = 1, std430) readonly buffer ssbo_in +{ + int State_in[]; +}; + +layout(binding = 2, std430) buffer ssbo_out +{ + int State_out[]; +}; + +uniform int u_GridSize; + +int CellIndex(vec2 CellCoord) +{ + return (int(CellCoord.y) * u_GridSize + int(CellCoord.x)); +} + +int cellActive(int x, int y) +{ + return int(State_in[CellIndex(vec2(x,y))]); +} + + +void main() +{ + /*if (State_in[CellIndex(gl_GlobalInvocationID.xy)] == 1) + { + State_out[CellIndex(gl_GlobalInvocationID.xy)] = 0; + } + + else + { + State_out[CellIndex(gl_GlobalInvocationID.xy)] = 1; + }*/ + + + State_out[CellIndex(gl_GlobalInvocationID.xy)] = State_in[CellIndex(vec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y + 1))]; + + +} \ No newline at end of file diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index a78eb88b..c88a2e24 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -11,18 +11,14 @@ SandboxLayer::SandboxLayer() { for (int i = 0; i < GRID_SIZE * GRID_SIZE; i++) { - int choices = 10; + int choices = 5; int picked_choice = (int)(rand() % choices); if (picked_choice == 1) { m_ParticleStates_in[i] = 1; } } - /* m_ParticleStates_in[500] = 1; - m_ParticleStates_in[500] = 1; - m_ParticleStates_in[500] = 1; - m_ParticleStates_in[500] = 1; - m_ParticleStates_in[500] = 1;*/ + } @@ -37,7 +33,7 @@ void SandboxLayer::OnAttach() m_shader = Shader::FromGLSLTextFiles("Shaders/vertex.vert.glsl", "Shaders/fragment.frag.glsl"); - m_comp_shader = CreateComputeShader("Shaders/compute.comp.glsl"); + m_comp_shader = CreateComputeShader("Shaders/GameOfLife.comp.glsl"); float vertices[] = { @@ -140,41 +136,32 @@ void SandboxLayer::OnEvent(Event& event) void SandboxLayer::OnUpdate(Timestep ts) { - - glClearColor(0.5f, 0.5f, 0.5f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); - glUseProgram(m_shader->GetRendererID()); - - - //set uniforms - int location = glGetUniformLocation(m_shader->GetRendererID(), "u_GridSize"); glUniform1i(location, m_GridSize); - glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0, GRID_SIZE * GRID_SIZE); - - - - - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); - - Sleep(100); + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0, GRID_SIZE * GRID_SIZE); - + Sleep(109); glUseProgram(m_comp_shader); - glDispatchCompute(GRID_SIZE / 8, GRID_SIZE / 8, 1); + location = glGetUniformLocation(m_comp_shader, "u_GridSize"); + glUniform1i(location, m_GridSize); + + glDispatchCompute(GRID_SIZE*GRID_SIZE,1, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); + std::swap(m_SSBO_in, m_SSBO_out); } diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index 6ca5ed26..5ed19135 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -1,6 +1,6 @@ #pragma once -#define GRID_SIZE 64 +#define GRID_SIZE 300 From 6ec5ff5ec9571f38eaa38b6ca123496b579f6372 Mon Sep 17 00:00:00 2001 From: Karl Riedewald Date: Tue, 23 Dec 2025 15:13:48 +0000 Subject: [PATCH 05/11] Window Resizing --- OpenGL-Core/src/GLCore/Core/Application.cpp | 9 +++++++++ OpenGL-Core/src/GLCore/Core/Application.h | 1 + OpenGL-Sandbox/src/RenderManager.cpp | 1 + OpenGL-Sandbox/src/RenderManager.h | 9 +++++++++ OpenGL-Sandbox/src/SandboxApp.cpp | 2 +- OpenGL-Sandbox/src/SandboxLayer.cpp | 10 ++++------ OpenGL-Sandbox/src/SandboxLayer.h | 2 +- 7 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 OpenGL-Sandbox/src/RenderManager.cpp create mode 100644 OpenGL-Sandbox/src/RenderManager.h diff --git a/OpenGL-Core/src/GLCore/Core/Application.cpp b/OpenGL-Core/src/GLCore/Core/Application.cpp index 8c81c184..71336a74 100644 --- a/OpenGL-Core/src/GLCore/Core/Application.cpp +++ b/OpenGL-Core/src/GLCore/Core/Application.cpp @@ -6,6 +6,7 @@ #include "Input.h" #include +#include namespace GLCore { @@ -47,6 +48,7 @@ namespace GLCore { { EventDispatcher dispatcher(e); dispatcher.Dispatch(BIND_EVENT_FN(OnWindowClose)); + dispatcher.Dispatch(BIND_EVENT_FN(OnWindowResize)); for (auto it = m_LayerStack.end(); it != m_LayerStack.begin(); ) { @@ -85,4 +87,11 @@ namespace GLCore { return true; } + bool Application::OnWindowResize(WindowResizeEvent& e) + { + + glViewport(0, 0, e.GetWidth(), e.GetHeight()); + return false; + } + } \ No newline at end of file diff --git a/OpenGL-Core/src/GLCore/Core/Application.h b/OpenGL-Core/src/GLCore/Core/Application.h index aea828f2..e1b0da32 100644 --- a/OpenGL-Core/src/GLCore/Core/Application.h +++ b/OpenGL-Core/src/GLCore/Core/Application.h @@ -31,6 +31,7 @@ namespace GLCore { inline static Application& Get() { return *s_Instance; } private: bool OnWindowClose(WindowCloseEvent& e); + bool OnWindowResize(WindowResizeEvent& e); private: std::unique_ptr m_Window; ImGuiLayer* m_ImGuiLayer; diff --git a/OpenGL-Sandbox/src/RenderManager.cpp b/OpenGL-Sandbox/src/RenderManager.cpp new file mode 100644 index 00000000..d45ad873 --- /dev/null +++ b/OpenGL-Sandbox/src/RenderManager.cpp @@ -0,0 +1 @@ +#include "RenderManager.h" diff --git a/OpenGL-Sandbox/src/RenderManager.h b/OpenGL-Sandbox/src/RenderManager.h new file mode 100644 index 00000000..64df72c8 --- /dev/null +++ b/OpenGL-Sandbox/src/RenderManager.h @@ -0,0 +1,9 @@ +#pragma once +#include +class RenderManager +{ +public: + RenderManager(Window* basewindow); + +}; + diff --git a/OpenGL-Sandbox/src/SandboxApp.cpp b/OpenGL-Sandbox/src/SandboxApp.cpp index e84829bc..67d5d4bf 100644 --- a/OpenGL-Sandbox/src/SandboxApp.cpp +++ b/OpenGL-Sandbox/src/SandboxApp.cpp @@ -7,7 +7,7 @@ class Sandbox : public Application { public: Sandbox() - :Application("Phase Change Simulation", 1000, 1000) + :Application("Phase Change Simulation", 1920, 1080) { PushLayer(new SandboxLayer()); } diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index c88a2e24..31f1c803 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -18,9 +18,6 @@ SandboxLayer::SandboxLayer() } } - - - } SandboxLayer::~SandboxLayer() @@ -130,8 +127,6 @@ void SandboxLayer::OnEvent(Event& event) return false; }); - - } void SandboxLayer::OnUpdate(Timestep ts) @@ -172,5 +167,8 @@ void SandboxLayer::OnRender() void SandboxLayer::OnImGuiRender() { - // ImGui here + ImGui::Begin("Viewport"); + + ImGui::End(); + } diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index 5ed19135..1dc7d40d 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -1,6 +1,6 @@ #pragma once -#define GRID_SIZE 300 +#define GRID_SIZE 600 From 9f325c7c54d8d6aab6df3739737e9d1f4a3cf4f3 Mon Sep 17 00:00:00 2001 From: karl2805 Date: Tue, 23 Dec 2025 17:19:51 +0000 Subject: [PATCH 06/11] camera working --- OpenGL-Core/src/GLCore/Core/Application.cpp | 10 ++ .../Util/OrthographicCameraController.cpp | 2 +- OpenGL-Sandbox/Shaders/vertex.vert.glsl | 4 +- OpenGL-Sandbox/src/RenderManager.cpp | 1 - OpenGL-Sandbox/src/RenderManager.h | 9 -- OpenGL-Sandbox/src/SandboxLayer.cpp | 109 ++++++++++-------- OpenGL-Sandbox/src/SandboxLayer.h | 23 +++- 7 files changed, 100 insertions(+), 58 deletions(-) delete mode 100644 OpenGL-Sandbox/src/RenderManager.cpp delete mode 100644 OpenGL-Sandbox/src/RenderManager.h diff --git a/OpenGL-Core/src/GLCore/Core/Application.cpp b/OpenGL-Core/src/GLCore/Core/Application.cpp index 71336a74..432d8625 100644 --- a/OpenGL-Core/src/GLCore/Core/Application.cpp +++ b/OpenGL-Core/src/GLCore/Core/Application.cpp @@ -8,6 +8,8 @@ #include #include +#include "KeyCodes.h" + namespace GLCore { #define BIND_EVENT_FN(x) std::bind(&Application::x, this, std::placeholders::_1) @@ -48,6 +50,14 @@ namespace GLCore { { EventDispatcher dispatcher(e); dispatcher.Dispatch(BIND_EVENT_FN(OnWindowClose)); + dispatcher.Dispatch([&](KeyPressedEvent& e) { + + if (e.GetKeyCode() == HZ_KEY_ESCAPE) + { + m_Running = false; + return false; + } + }); dispatcher.Dispatch(BIND_EVENT_FN(OnWindowResize)); for (auto it = m_LayerStack.end(); it != m_LayerStack.begin(); ) diff --git a/OpenGL-Core/src/GLCore/Util/OrthographicCameraController.cpp b/OpenGL-Core/src/GLCore/Util/OrthographicCameraController.cpp index b7f37144..2fe0d3a6 100644 --- a/OpenGL-Core/src/GLCore/Util/OrthographicCameraController.cpp +++ b/OpenGL-Core/src/GLCore/Util/OrthographicCameraController.cpp @@ -65,7 +65,7 @@ namespace GLCore::Utils { bool OrthographicCameraController::OnMouseScrolled(MouseScrolledEvent& e) { m_ZoomLevel -= e.GetYOffset() * 0.25f; - m_ZoomLevel = std::max(m_ZoomLevel, 0.25f); + m_ZoomLevel = std::max(m_ZoomLevel, 0.01f); m_Camera.SetProjection(-m_AspectRatio * m_ZoomLevel, m_AspectRatio * m_ZoomLevel, -m_ZoomLevel, m_ZoomLevel); return false; } diff --git a/OpenGL-Sandbox/Shaders/vertex.vert.glsl b/OpenGL-Sandbox/Shaders/vertex.vert.glsl index 47f6546c..a997bb59 100644 --- a/OpenGL-Sandbox/Shaders/vertex.vert.glsl +++ b/OpenGL-Sandbox/Shaders/vertex.vert.glsl @@ -20,6 +20,8 @@ vec2 cell = vec2(i % u_GridSize, floor(i / u_GridSize)); int State = data_SSBO[gl_InstanceID]; +uniform mat4 u_ViewProjection; + @@ -33,6 +35,6 @@ void main() - gl_Position = vec4(position, 0.0f, 1.0f); + gl_Position = u_ViewProjection * vec4(position, 0.0f, 1.0f); } \ No newline at end of file diff --git a/OpenGL-Sandbox/src/RenderManager.cpp b/OpenGL-Sandbox/src/RenderManager.cpp deleted file mode 100644 index d45ad873..00000000 --- a/OpenGL-Sandbox/src/RenderManager.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "RenderManager.h" diff --git a/OpenGL-Sandbox/src/RenderManager.h b/OpenGL-Sandbox/src/RenderManager.h deleted file mode 100644 index 64df72c8..00000000 --- a/OpenGL-Sandbox/src/RenderManager.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include -class RenderManager -{ -public: - RenderManager(Window* basewindow); - -}; - diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index 31f1c803..03e60be0 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -8,15 +8,9 @@ using namespace GLCore; using namespace GLCore::Utils; SandboxLayer::SandboxLayer() + : m_CameraController(1.0f / 1.0f) { - for (int i = 0; i < GRID_SIZE * GRID_SIZE; i++) - { - int choices = 5; - int picked_choice = (int)(rand() % choices); - if (picked_choice == 1) { - m_ParticleStates_in[i] = 1; - } - } + } @@ -32,19 +26,14 @@ void SandboxLayer::OnAttach() m_comp_shader = CreateComputeShader("Shaders/GameOfLife.comp.glsl"); - - float vertices[] = { - // positions - 1.0f, 1.0f, 0.0f, - 1.0f, -1.0f, 0.0f, - -1.0f, -1.0f, 0.0f, - -1.0f, 1.0f, 0.0f - }; - - unsigned int indices[] = { - 0, 1, 3, // first triangle - 1, 2, 3 // second triangle - }; + for (int i = 0; i < m_GridSize * m_GridSize; i++) + { + int choices = 5; + int picked_choice = (int)(rand() % choices); + if (picked_choice == 1) { + m_ParticleStates_in[i] = 1; + } + } glGenVertexArrays(1, &m_VAO); glGenBuffers(1, &m_VBO); @@ -80,16 +69,23 @@ void SandboxLayer::OnAttach() void SandboxLayer::OnDetach() { - // Shutdown here -} + glDeleteBuffers(sizeof(vertices), &m_VBO); + glDeleteBuffers(sizeof(indices), &m_EBO); -int CellIndex(glm::vec2 CellCoord) -{ - return int(CellCoord.y) * GRID_SIZE + int(CellCoord.x) * 0.001; + + + //set the ping pong arrays to zero + memset(m_ParticleStates_in, 0, sizeof(m_ParticleStates_in)); + memset(m_ParticleStates_out, 0, sizeof(m_ParticleStates_out)); } + + void SandboxLayer::OnEvent(Event& event) { + + m_CameraController.OnEvent(event); + EventDispatcher dispatcher(event); dispatcher.Dispatch( @@ -113,24 +109,22 @@ void SandboxLayer::OnEvent(Event& event) std::cout << "preessed"; glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_in), m_ParticleStates_in, GL_DYNAMIC_STORAGE_BIT); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_in), m_ParticleStates_in, GL_STATIC_DRAW); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); return false; }); - dispatcher.Dispatch( - [&](KeyPressedEvent&e) - { - m_Compute = m_Compute ? false : true; + + - return false; - }); + } void SandboxLayer::OnUpdate(Timestep ts) { + m_CameraController.OnUpdate(ts); glClearColor(0.5f, 0.5f, 0.5f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); @@ -139,25 +133,32 @@ void SandboxLayer::OnUpdate(Timestep ts) int location = glGetUniformLocation(m_shader->GetRendererID(), "u_GridSize"); glUniform1i(location, m_GridSize); - + location = glGetUniformLocation(m_shader->GetRendererID(), "u_ViewProjection"); + glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(m_CameraController.GetCamera().GetViewProjectionMatrix())); - glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0, GRID_SIZE * GRID_SIZE); - - Sleep(109); + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0, m_GridSize * m_GridSize); - glUseProgram(m_comp_shader); + m_TimestepLogger += ts; - location = glGetUniformLocation(m_comp_shader, "u_GridSize"); - glUniform1i(location, m_GridSize); + if (m_TimestepLogger >= m_UpdateFrequency && m_Compute) + { - glDispatchCompute(GRID_SIZE*GRID_SIZE,1, 1); - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + glUseProgram(m_comp_shader); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + location = glGetUniformLocation(m_comp_shader, "u_GridSize"); + glUniform1i(location, m_GridSize); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); + glDispatchCompute(m_GridSize * m_GridSize, 1, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); + + std::swap(m_SSBO_in, m_SSBO_out); - std::swap(m_SSBO_in, m_SSBO_out); + m_TimestepLogger = 0; + } } void SandboxLayer::OnRender() @@ -169,6 +170,24 @@ void SandboxLayer::OnImGuiRender() { ImGui::Begin("Viewport"); + + + if (ImGui::Button(m_State.c_str())) + { + m_Compute = m_Compute ? false : true; + m_State = m_Compute ? "Pause Simulation" : "Run Simulation"; + } + + if (ImGui::Button("Reset")) + { + OnDetach(); + OnAttach(); + } + + + ImGui::SliderFloat("Speed", &m_UpdateFrequency, 0.2f, 0.0f); + ImGui::SliderInt("GridSize", &m_GridSize, 4, 1000); + ImGui::End(); } diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index 1dc7d40d..03d3e937 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -1,6 +1,6 @@ #pragma once -#define GRID_SIZE 600 +#define GRID_SIZE 100 @@ -28,6 +28,22 @@ class SandboxLayer : public GLCore::Layer GLCore::Utils::Shader* m_shader; + float vertices[3 * 4] = { + // positions + 1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 0.0f, + -1.0f, -1.0f, 0.0f, + -1.0f, 1.0f, 0.0f + }; + + unsigned int indices[3 * 3] = { + 0, 1, 3, // first triangle + 1, 2, 3 // second triangle + }; + + GLCore::Utils::OrthographicCameraController m_CameraController; + + GLint m_comp_shader; int m_WindowHeight = 720; @@ -37,6 +53,11 @@ class SandboxLayer : public GLCore::Layer glm::vec2 m_MousePos; + float m_TimestepLogger = 0; + float m_UpdateFrequency = 0.01; + + std::string m_State = "Run Simulation"; + int m_GridSize = GRID_SIZE; From 7ff3b6f8bd8db71b9f76b5d0fc65a28936ba60f9 Mon Sep 17 00:00:00 2001 From: karl2805 Date: Tue, 23 Dec 2025 21:06:10 +0000 Subject: [PATCH 07/11] trying to move rendering to seperate class --- OpenGL-Sandbox/src/GameOfLife.cpp | 110 +++++++++++++++++++++ OpenGL-Sandbox/src/GameOfLife.h | 49 ++++++++++ OpenGL-Sandbox/src/SandboxLayer.cpp | 147 ++-------------------------- OpenGL-Sandbox/src/SandboxLayer.h | 52 ++-------- 4 files changed, 174 insertions(+), 184 deletions(-) create mode 100644 OpenGL-Sandbox/src/GameOfLife.cpp create mode 100644 OpenGL-Sandbox/src/GameOfLife.h diff --git a/OpenGL-Sandbox/src/GameOfLife.cpp b/OpenGL-Sandbox/src/GameOfLife.cpp new file mode 100644 index 00000000..1feb8995 --- /dev/null +++ b/OpenGL-Sandbox/src/GameOfLife.cpp @@ -0,0 +1,110 @@ +#include "GameOfLife.h" + +using namespace GLCore; +using namespace GLCore::Utils; + +GameOfLife::GameOfLife() + :m_CameraController(1.0f) +{ + + m_Shader = Shader::FromGLSLTextFiles("Shaders/vertex.vert.glsl", "Shaders/fragment.frag.glsl"); + + m_CompShader = CreateComputeShader("Shaders/GameOfLife.comp.glsl"); + + + for (int i = 0; i < m_GridSize * m_GridSize; i++) + { + int choices = 5; + int picked_choice = (int)(rand() % choices); + if (picked_choice == 1) { + m_ParticleStates_in[i] = 1; + } + } + + float vertices[3 * 4] = { + // positions + 1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 0.0f, + -1.0f, -1.0f, 0.0f, + -1.0f, 1.0f, 0.0f + }; + + glGenVertexArrays(1, &m_VAO); + glGenBuffers(1, &m_VBO); + glGenBuffers(1, &m_EBO); + + glBindVertexArray(m_VAO); + + glBindBuffer(GL_ARRAY_BUFFER, m_VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + unsigned int indices[3 * 3] = { + 0, 1, 3, // first triangle + 1, 2, 3 // second triangle + }; + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // position attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + + //create SSBO Buffer + glGenBuffers(1, &m_SSBO_in); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_in), m_ParticleStates_in.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + glGenBuffers(1, &m_SSBO_out); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_out); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_out), m_ParticleStates_out.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); +} + +GameOfLife::~GameOfLife() +{ + +} + +void GameOfLife::OnCompute() +{ + glUseProgram(m_CompShader); + + int location = glGetUniformLocation(m_CompShader, "u_GridSize"); + glUniform1i(location, m_GridSize); + + glDispatchCompute(m_GridSize * m_GridSize, 1, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); + + std::swap(m_SSBO_in, m_SSBO_out); +} + +void GameOfLife::OnRender(Timestep ts) +{ + m_CameraController.OnUpdate(ts); + glClearColor(0.5f, 0.5f, 0.5f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + glUseProgram(m_Shader->GetRendererID()); + + int location = glGetUniformLocation(m_Shader->GetRendererID(), "u_GridSize"); + glUniform1i(location, m_GridSize); + + location = glGetUniformLocation(m_Shader->GetRendererID(), "u_ViewProjection"); + glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(m_CameraController.GetCamera().GetViewProjectionMatrix())); + + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0, m_GridSize * m_GridSize); +} + +void GameOfLife::OnEvent(Event& event) +{ + m_CameraController.OnEvent(event); +} diff --git a/OpenGL-Sandbox/src/GameOfLife.h b/OpenGL-Sandbox/src/GameOfLife.h new file mode 100644 index 00000000..c74816b7 --- /dev/null +++ b/OpenGL-Sandbox/src/GameOfLife.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include "GLCore/Util/Texture.h" +#include "GLCore/Util/Shader.h" + +#define GRID_SIZE 100 + +using namespace GLCore; +using namespace GLCore::Utils; + +class GameOfLife +{ +public: + + GameOfLife(); + ~GameOfLife(); + + void OnCompute(); + void OnRender(Timestep ts); + void OnEvent(Event& e); + +private: + int m_GridSize = GRID_SIZE; + + + //SSBO containers + std::array m_ParticleStates_in = { 0 }; + std::array m_ParticleStates_out = { 0 }; + + + GLCore::Utils::OrthographicCameraController m_CameraController; + + + + + + + + //Drawing Stuff + GLuint m_VBO, m_VAO, m_EBO; + GLuint m_SSBO_in, m_SSBO_out; + GLuint m_CompShader; + GLCore::Utils::Shader* m_Shader; + + +}; + diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index 03e60be0..490ea4f3 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -1,82 +1,35 @@ #include "SandboxLayer.h" -#include "GLCore/Util/Shader.h" -#include -#include -#include "GLCore/Core/KeyCodes.h" + + using namespace GLCore; using namespace GLCore::Utils; SandboxLayer::SandboxLayer() - : m_CameraController(1.0f / 1.0f) { + } SandboxLayer::~SandboxLayer() { + } void SandboxLayer::OnAttach() { EnableGLDebugging(); - m_shader = Shader::FromGLSLTextFiles("Shaders/vertex.vert.glsl", "Shaders/fragment.frag.glsl"); - - m_comp_shader = CreateComputeShader("Shaders/GameOfLife.comp.glsl"); - - for (int i = 0; i < m_GridSize * m_GridSize; i++) - { - int choices = 5; - int picked_choice = (int)(rand() % choices); - if (picked_choice == 1) { - m_ParticleStates_in[i] = 1; - } - } - glGenVertexArrays(1, &m_VAO); - glGenBuffers(1, &m_VBO); - glGenBuffers(1, &m_EBO); - - glBindVertexArray(m_VAO); - - glBindBuffer(GL_ARRAY_BUFFER, m_VBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - - // position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - //create SSBO Buffer - glGenBuffers(1, &m_SSBO_in); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_in), m_ParticleStates_in, GL_STATIC_DRAW); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - - glGenBuffers(1, &m_SSBO_out); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_out); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_out), m_ParticleStates_out, GL_STATIC_DRAW); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); } void SandboxLayer::OnDetach() { - glDeleteBuffers(sizeof(vertices), &m_VBO); - glDeleteBuffers(sizeof(indices), &m_EBO); - - - - //set the ping pong arrays to zero - memset(m_ParticleStates_in, 0, sizeof(m_ParticleStates_in)); - memset(m_ParticleStates_out, 0, sizeof(m_ParticleStates_out)); + } @@ -84,81 +37,16 @@ void SandboxLayer::OnDetach() void SandboxLayer::OnEvent(Event& event) { - m_CameraController.OnEvent(event); - - EventDispatcher dispatcher(event); - - dispatcher.Dispatch( - [&](MouseMovedEvent& e) - { - m_MousePos.x = e.GetX(); - m_MousePos.y = e.GetY(); - - std::cout << m_MousePos.x << " " << m_MousePos.y << "\n"; - - return false; - }); - - dispatcher.Dispatch( - [&](MouseButtonPressedEvent& e) - { - - m_ParticleStates_in[1000] = 1; - m_ParticleStates_out[1000] = 1; - - std::cout << "preessed"; - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_in), m_ParticleStates_in, GL_STATIC_DRAW); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); - - return false; - }); - - - - - } void SandboxLayer::OnUpdate(Timestep ts) { - m_CameraController.OnUpdate(ts); - glClearColor(0.5f, 0.5f, 0.5f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - - glUseProgram(m_shader->GetRendererID()); - - int location = glGetUniformLocation(m_shader->GetRendererID(), "u_GridSize"); - glUniform1i(location, m_GridSize); - - location = glGetUniformLocation(m_shader->GetRendererID(), "u_ViewProjection"); - glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(m_CameraController.GetCamera().GetViewProjectionMatrix())); - - glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0, m_GridSize * m_GridSize); - m_TimestepLogger += ts; - - if (m_TimestepLogger >= m_UpdateFrequency && m_Compute) - { - - glUseProgram(m_comp_shader); - - location = glGetUniformLocation(m_comp_shader, "u_GridSize"); - glUniform1i(location, m_GridSize); - - glDispatchCompute(m_GridSize * m_GridSize, 1, 1); - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); - - std::swap(m_SSBO_in, m_SSBO_out); + + - m_TimestepLogger = 0; - } + } void SandboxLayer::OnRender() @@ -168,26 +56,9 @@ void SandboxLayer::OnRender() void SandboxLayer::OnImGuiRender() { - ImGui::Begin("Viewport"); + - if (ImGui::Button(m_State.c_str())) - { - m_Compute = m_Compute ? false : true; - m_State = m_Compute ? "Pause Simulation" : "Run Simulation"; - } - - if (ImGui::Button("Reset")) - { - OnDetach(); - OnAttach(); - } - - - ImGui::SliderFloat("Speed", &m_UpdateFrequency, 0.2f, 0.0f); - ImGui::SliderInt("GridSize", &m_GridSize, 4, 1000); - - ImGui::End(); } diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index 03d3e937..3700bb56 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -1,14 +1,9 @@ #pragma once -#define GRID_SIZE 100 - - - #include -#include #include "GLCore/Util/Texture.h" -#include "GLCore/Util/Shader.h" +#include "GameOfLife.h" class SandboxLayer : public GLCore::Layer { @@ -23,49 +18,14 @@ class SandboxLayer : public GLCore::Layer virtual void OnRender() override; virtual void OnImGuiRender() override; private: - Texture m_texture; - GLuint m_VBO, m_VAO, m_EBO, m_SSBO_in, m_SSBO_out; - GLCore::Utils::Shader* m_shader; - - - float vertices[3 * 4] = { - // positions - 1.0f, 1.0f, 0.0f, - 1.0f, -1.0f, 0.0f, - -1.0f, -1.0f, 0.0f, - -1.0f, 1.0f, 0.0f - }; + + + + GameOfLife m_Game; - unsigned int indices[3 * 3] = { - 0, 1, 3, // first triangle - 1, 2, 3 // second triangle - }; - - GLCore::Utils::OrthographicCameraController m_CameraController; - - - GLint m_comp_shader; int m_WindowHeight = 720; int m_WindowWidth = 1280; - GLuint tex_output; - - glm::vec2 m_MousePos; - - float m_TimestepLogger = 0; - float m_UpdateFrequency = 0.01; - - std::string m_State = "Run Simulation"; - - - int m_GridSize = GRID_SIZE; - - int m_ParticleStates_in[GRID_SIZE * GRID_SIZE] = { 0 }; - int m_ParticleStates_out[GRID_SIZE * GRID_SIZE] = { 0 }; - - bool m_Compute = false; - - int m_Counter = 0; - + }; \ No newline at end of file From 6cb4eb9272630b2eb8b6f8c0186ce76076e446b8 Mon Sep 17 00:00:00 2001 From: Karl Riedewald Date: Wed, 24 Dec 2025 02:06:38 +0000 Subject: [PATCH 08/11] Moved to new Class --- OpenGL-Sandbox/src/GameOfLife.cpp | 76 +++++++++++++++++++++++------ OpenGL-Sandbox/src/GameOfLife.h | 24 +++++---- OpenGL-Sandbox/src/SandboxApp.cpp | 2 +- OpenGL-Sandbox/src/SandboxLayer.cpp | 49 +++++++++++++++++-- OpenGL-Sandbox/src/SandboxLayer.h | 28 +++++++++-- 5 files changed, 139 insertions(+), 40 deletions(-) diff --git a/OpenGL-Sandbox/src/GameOfLife.cpp b/OpenGL-Sandbox/src/GameOfLife.cpp index 1feb8995..57db1890 100644 --- a/OpenGL-Sandbox/src/GameOfLife.cpp +++ b/OpenGL-Sandbox/src/GameOfLife.cpp @@ -3,23 +3,16 @@ using namespace GLCore; using namespace GLCore::Utils; -GameOfLife::GameOfLife() - :m_CameraController(1.0f) +GameOfLife::GameOfLife(int GridSize) + :m_CameraController(1.0f), m_GridSize(GridSize), + m_ParticleStates_in(GridSize * GridSize), + m_ParticleStates_out(GridSize * GridSize) { - m_Shader = Shader::FromGLSLTextFiles("Shaders/vertex.vert.glsl", "Shaders/fragment.frag.glsl"); m_CompShader = CreateComputeShader("Shaders/GameOfLife.comp.glsl"); - - for (int i = 0; i < m_GridSize * m_GridSize; i++) - { - int choices = 5; - int picked_choice = (int)(rand() % choices); - if (picked_choice == 1) { - m_ParticleStates_in[i] = 1; - } - } + //RandomGrid(); float vertices[3 * 4] = { // positions @@ -54,19 +47,27 @@ GameOfLife::GameOfLife() //create SSBO Buffer glGenBuffers(1, &m_SSBO_in); glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_in), m_ParticleStates_in.data(), GL_STATIC_DRAW); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates_in.size() * sizeof(int), m_ParticleStates_in.data(), GL_STATIC_DRAW); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); glGenBuffers(1, &m_SSBO_out); glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_out); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(m_ParticleStates_out), m_ParticleStates_out.data(), GL_STATIC_DRAW); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates_out.size() * sizeof(int), m_ParticleStates_out.data(), GL_STATIC_DRAW); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); } GameOfLife::~GameOfLife() { + GLuint buffers[] = { m_VBO, m_EBO, m_SSBO_in, m_SSBO_out }; + glDeleteBuffers(IM_ARRAYSIZE(buffers), buffers); + + glDeleteVertexArrays(1, &m_VAO); + + glDeleteProgram(m_Shader->GetRendererID()); + glDeleteProgram(m_CompShader); + } @@ -77,7 +78,7 @@ void GameOfLife::OnCompute() int location = glGetUniformLocation(m_CompShader, "u_GridSize"); glUniform1i(location, m_GridSize); - glDispatchCompute(m_GridSize * m_GridSize, 1, 1); + glDispatchCompute(m_GridSize / 8, m_GridSize / 8, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); @@ -101,10 +102,53 @@ void GameOfLife::OnRender(Timestep ts) location = glGetUniformLocation(m_Shader->GetRendererID(), "u_ViewProjection"); glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(m_CameraController.GetCamera().GetViewProjectionMatrix())); - glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0, m_GridSize * m_GridSize); + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr, m_GridSize * m_GridSize); } void GameOfLife::OnEvent(Event& event) { m_CameraController.OnEvent(event); } + +void GameOfLife::RandomGrid() +{ + for (int i = 0; i < m_GridSize * m_GridSize; i++) + { + int choices = 5; + int picked_choice = (int)(rand() % choices); + if (picked_choice == 1) { + m_ParticleStates_in.at(i) = 1; + } + } +} + + +void GameOfLife::AddCell(int xcoord, int ycoord) +{ + int index = GetCellIndex(xcoord, ycoord, this); + m_ParticleStates_in.at(index) = 1; + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates_in.size() * sizeof(int), m_ParticleStates_in.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); +} + +int GetCellIndex(int xcoord, int ycoord, GameOfLife* game) +{ + int gridsize = game->m_GridSize; + + glm::vec2 CellCoord = glm::vec2(xcoord / gridsize, (ycoord - 1000)/ gridsize); + + + return (CellCoord.y) * gridsize + (CellCoord.x); +} + +GameOfLife* ChangeGridSize(GameOfLife* game, int newGridSize) +{ + delete game; + + GameOfLife* newGame = new GameOfLife(newGridSize); + + return newGame; + +} diff --git a/OpenGL-Sandbox/src/GameOfLife.h b/OpenGL-Sandbox/src/GameOfLife.h index c74816b7..d5f4e846 100644 --- a/OpenGL-Sandbox/src/GameOfLife.h +++ b/OpenGL-Sandbox/src/GameOfLife.h @@ -10,33 +10,29 @@ using namespace GLCore; using namespace GLCore::Utils; + + class GameOfLife { + friend int GetCellIndex(int xcoord, int ycoord, GameOfLife* game); public: - GameOfLife(); + GameOfLife(int GridSize); ~GameOfLife(); void OnCompute(); void OnRender(Timestep ts); void OnEvent(Event& e); + void RandomGrid(); + void AddCell(int xcoord, int ycoord); private: - int m_GridSize = GRID_SIZE; - - - //SSBO containers - std::array m_ParticleStates_in = { 0 }; - std::array m_ParticleStates_out = { 0 }; - + int m_GridSize; GLCore::Utils::OrthographicCameraController m_CameraController; - - - - - + std::vector m_ParticleStates_in; + std::vector m_ParticleStates_out; //Drawing Stuff GLuint m_VBO, m_VAO, m_EBO; @@ -47,3 +43,5 @@ class GameOfLife }; +GameOfLife* ChangeGridSize(GameOfLife* game, int newGridSize); + diff --git a/OpenGL-Sandbox/src/SandboxApp.cpp b/OpenGL-Sandbox/src/SandboxApp.cpp index 67d5d4bf..e84829bc 100644 --- a/OpenGL-Sandbox/src/SandboxApp.cpp +++ b/OpenGL-Sandbox/src/SandboxApp.cpp @@ -7,7 +7,7 @@ class Sandbox : public Application { public: Sandbox() - :Application("Phase Change Simulation", 1920, 1080) + :Application("Phase Change Simulation", 1000, 1000) { PushLayer(new SandboxLayer()); } diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index 490ea4f3..7e397b2a 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -5,6 +5,8 @@ using namespace GLCore; using namespace GLCore::Utils; + + SandboxLayer::SandboxLayer() { @@ -14,13 +16,14 @@ SandboxLayer::SandboxLayer() SandboxLayer::~SandboxLayer() { + delete m_Game; } void SandboxLayer::OnAttach() { EnableGLDebugging(); - + @@ -36,17 +39,45 @@ void SandboxLayer::OnDetach() void SandboxLayer::OnEvent(Event& event) { + m_Game->OnEvent(event); + + EventDispatcher dispatcher(event); + + dispatcher.Dispatch([&](MouseMovedEvent& e) + { + m_XMousePos = e.GetX(); + m_YMousePos = e.GetY(); + std::cout << m_XMousePos << " " << m_YMousePos << "\n"; + + return false; + }); + + dispatcher.Dispatch([&](MouseButtonPressedEvent& e) + { + if (e.GetMouseButton() == RightClick) + { + m_Game->AddCell(m_XMousePos, m_YMousePos); + + + } + return false; + }); } void SandboxLayer::OnUpdate(Timestep ts) { + if (m_ComputeState == COMPUTE && m_TimeCounter >= m_UpdateFrequency) + { + m_Game->OnCompute(); + m_TimeCounter = 0; + } - + m_Game->OnRender(ts); - + m_TimeCounter += ts; } void SandboxLayer::OnRender() @@ -56,9 +87,17 @@ void SandboxLayer::OnRender() void SandboxLayer::OnImGuiRender() { - + if (ImGui::Button("Start")) + { + m_ComputeState = m_ComputeState == COMPUTE ? PAUSED : COMPUTE; + } + + if (ImGui::SliderInt("GridSize", &m_GridSize, 4, 1000)) + { + m_ComputeState = PAUSED; + m_Game = ChangeGridSize(m_Game, m_GridSize); + } - } diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index 3700bb56..b1597a16 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -1,10 +1,21 @@ #pragma once -#include -#include "GLCore/Util/Texture.h" + #include "GameOfLife.h" +enum State +{ + COMPUTE, + PAUSED +}; + +enum MouseButton +{ + RightClick = 0, + LeftClick +}; + class SandboxLayer : public GLCore::Layer { public: @@ -21,11 +32,18 @@ class SandboxLayer : public GLCore::Layer - GameOfLife m_Game; + GameOfLife* m_Game = new GameOfLife(64); + + State m_ComputeState = PAUSED; + float m_UpdateFrequency = 0.05f; + float m_TimeCounter = 0; + + int m_GridSize = 64; + + float m_XMousePos; + float m_YMousePos; - int m_WindowHeight = 720; - int m_WindowWidth = 1280; }; \ No newline at end of file From 94bc6e8a020d9062911f943b38cfac2717f3810a Mon Sep 17 00:00:00 2001 From: karl2805 Date: Wed, 24 Dec 2025 16:33:49 +0000 Subject: [PATCH 09/11] added grid and proper clicking --- ...ragment.frag.glsl => CellRender.frag.glsl} | 8 -- ...{vertex.vert.glsl => CellRender.vert.glsl} | 0 OpenGL-Sandbox/Shaders/GameOfLife.comp.glsl | 22 +++--- OpenGL-Sandbox/Shaders/GridRender.frag.glsl | 28 +++++++ OpenGL-Sandbox/Shaders/GridRender.vert.glsl | 38 ++++++++++ OpenGL-Sandbox/Shaders/Swap.comp.glsl | 44 ----------- OpenGL-Sandbox/src/GameOfLife.cpp | 74 +++++++++++++++++-- OpenGL-Sandbox/src/GameOfLife.h | 11 ++- OpenGL-Sandbox/src/GridLayer.cpp | 1 + OpenGL-Sandbox/src/GridLayer.h | 7 ++ OpenGL-Sandbox/src/SandboxLayer.cpp | 18 ++++- 11 files changed, 179 insertions(+), 72 deletions(-) rename OpenGL-Sandbox/Shaders/{fragment.frag.glsl => CellRender.frag.glsl} (96%) rename OpenGL-Sandbox/Shaders/{vertex.vert.glsl => CellRender.vert.glsl} (100%) create mode 100644 OpenGL-Sandbox/Shaders/GridRender.frag.glsl create mode 100644 OpenGL-Sandbox/Shaders/GridRender.vert.glsl delete mode 100644 OpenGL-Sandbox/Shaders/Swap.comp.glsl create mode 100644 OpenGL-Sandbox/src/GridLayer.cpp create mode 100644 OpenGL-Sandbox/src/GridLayer.h diff --git a/OpenGL-Sandbox/Shaders/fragment.frag.glsl b/OpenGL-Sandbox/Shaders/CellRender.frag.glsl similarity index 96% rename from OpenGL-Sandbox/Shaders/fragment.frag.glsl rename to OpenGL-Sandbox/Shaders/CellRender.frag.glsl index 6f9a53ce..06c0eafd 100644 --- a/OpenGL-Sandbox/Shaders/fragment.frag.glsl +++ b/OpenGL-Sandbox/Shaders/CellRender.frag.glsl @@ -1,13 +1,7 @@ #version 440 core - - - in vec2 v_LocalPosition; - - - out vec4 FragColor; float BorderWidth = 0.0f; @@ -15,7 +9,6 @@ float BorderWidth = 0.0f; void main() { - FragColor = vec4(0.08, 0.33, 0.78, 1.0); //set border @@ -25,5 +18,4 @@ void main() FragColor = vec4(0.0, 0.0, 0.0, 1.0); } - } \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/vertex.vert.glsl b/OpenGL-Sandbox/Shaders/CellRender.vert.glsl similarity index 100% rename from OpenGL-Sandbox/Shaders/vertex.vert.glsl rename to OpenGL-Sandbox/Shaders/CellRender.vert.glsl diff --git a/OpenGL-Sandbox/Shaders/GameOfLife.comp.glsl b/OpenGL-Sandbox/Shaders/GameOfLife.comp.glsl index 35032e61..66834c67 100644 --- a/OpenGL-Sandbox/Shaders/GameOfLife.comp.glsl +++ b/OpenGL-Sandbox/Shaders/GameOfLife.comp.glsl @@ -27,9 +27,9 @@ int cellActive(int x, int y) void main() { - vec2 cell = gl_GlobalInvocationID.xy; +vec2 cell = gl_GlobalInvocationID.xy; - int activeNeighbors = cellActive(int(cell.x+1), int(cell.y+1)) + +int activeNeighbors = cellActive(int(cell.x+1), int(cell.y+1)) + cellActive(int(cell.x+1), int(cell.y)) + cellActive(int(cell.x+1), int(cell.y-1)) + cellActive(int(cell.x), int(cell.y-1)) + @@ -40,21 +40,25 @@ void main() { int i = CellIndex(cell); -switch (activeNeighbors) { - case 2: { // Active cells with 2 neighbors stay active. +switch (activeNeighbors) +{ + case 2: + { // Active cells with 2 neighbors stay active. State_out[i] = State_in[i]; break; } - case 3: { // Cells with 3 neighbors become or stay active. + case 3: + { // Cells with 3 neighbors become or stay active. State_out[i] = 1; break; } - default: { // Cells with < 2 or > 3 neighbors become inactive. - State_out[i] = 0; - } - + default: + { // Cells with < 2 or > 3 neighbors become inactive. + State_out[i] = 0; } +} + } \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/GridRender.frag.glsl b/OpenGL-Sandbox/Shaders/GridRender.frag.glsl new file mode 100644 index 00000000..694c80cf --- /dev/null +++ b/OpenGL-Sandbox/Shaders/GridRender.frag.glsl @@ -0,0 +1,28 @@ +#version 440 core + +in vec2 v_LocalPosition; + +out vec4 FragColor; + +float BorderWidth = 0.1f; + +uniform vec4 u_ClearColor; + +void main() +{ + + FragColor = u_ClearColor; + + //set border + if (v_LocalPosition.x < -1 + BorderWidth || v_LocalPosition.x > 1 - BorderWidth || + v_LocalPosition.y < -1 + BorderWidth || v_LocalPosition.y > 1 - BorderWidth) + { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); + } + + else + { + discard; + } + +} \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/GridRender.vert.glsl b/OpenGL-Sandbox/Shaders/GridRender.vert.glsl new file mode 100644 index 00000000..0f59ebfc --- /dev/null +++ b/OpenGL-Sandbox/Shaders/GridRender.vert.glsl @@ -0,0 +1,38 @@ +#version 430 core + + +layout (location = 0) in vec2 aPos; + +layout(std430, binding = 3) buffer layoutName +{ + int data_SSBO[]; +}; + + +out vec2 v_LocalPosition; +uniform int u_GridSize; + +float temp = 0.9f; + +int i = int(gl_InstanceID); + +vec2 cell = vec2(i % u_GridSize, floor(i / u_GridSize)); + +int State = data_SSBO[gl_InstanceID]; + +uniform mat4 u_ViewProjection; + + + + +void main() +{ + + v_LocalPosition = aPos; + vec2 position = (aPos + 1) / u_GridSize - 1; + + position = position * State + 2 * (cell / u_GridSize); + + gl_Position = u_ViewProjection * vec4(position, 0.0f, 1.0f); + +} \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/Swap.comp.glsl b/OpenGL-Sandbox/Shaders/Swap.comp.glsl deleted file mode 100644 index 9a683737..00000000 --- a/OpenGL-Sandbox/Shaders/Swap.comp.glsl +++ /dev/null @@ -1,44 +0,0 @@ -#version 430 - -layout(local_size_x = 8, local_size_y = 8) in; - -layout(binding = 1, std430) readonly buffer ssbo_in -{ - int State_in[]; -}; - -layout(binding = 2, std430) buffer ssbo_out -{ - int State_out[]; -}; - -uniform int u_GridSize; - -int CellIndex(vec2 CellCoord) -{ - return (int(CellCoord.y) * u_GridSize + int(CellCoord.x)); -} - -int cellActive(int x, int y) -{ - return int(State_in[CellIndex(vec2(x,y))]); -} - - -void main() -{ - /*if (State_in[CellIndex(gl_GlobalInvocationID.xy)] == 1) - { - State_out[CellIndex(gl_GlobalInvocationID.xy)] = 0; - } - - else - { - State_out[CellIndex(gl_GlobalInvocationID.xy)] = 1; - }*/ - - - State_out[CellIndex(gl_GlobalInvocationID.xy)] = State_in[CellIndex(vec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y + 1))]; - - -} \ No newline at end of file diff --git a/OpenGL-Sandbox/src/GameOfLife.cpp b/OpenGL-Sandbox/src/GameOfLife.cpp index 57db1890..953b4dc6 100644 --- a/OpenGL-Sandbox/src/GameOfLife.cpp +++ b/OpenGL-Sandbox/src/GameOfLife.cpp @@ -5,14 +5,19 @@ using namespace GLCore::Utils; GameOfLife::GameOfLife(int GridSize) :m_CameraController(1.0f), m_GridSize(GridSize), - m_ParticleStates_in(GridSize * GridSize), - m_ParticleStates_out(GridSize * GridSize) + m_ParticleStates_in(GridSize* GridSize), + m_ParticleStates_out(GridSize* GridSize), + m_Grid(GridSize* GridSize) { - m_Shader = Shader::FromGLSLTextFiles("Shaders/vertex.vert.glsl", "Shaders/fragment.frag.glsl"); + m_Shader = Shader::FromGLSLTextFiles("Shaders/CellRender.vert.glsl", "Shaders/CellRender.frag.glsl"); + + m_GridShader = Shader::FromGLSLTextFiles("Shaders/GridRender.vert.glsl", "Shaders/GridRender.frag.glsl"); m_CompShader = CreateComputeShader("Shaders/GameOfLife.comp.glsl"); - //RandomGrid(); + + FillGrid(); + RandomGrid(); float vertices[3 * 4] = { // positions @@ -56,6 +61,12 @@ GameOfLife::GameOfLife(int GridSize) glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates_out.size() * sizeof(int), m_ParticleStates_out.data(), GL_STATIC_DRAW); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + glGenBuffers(1, &m_SSBO_grid); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_grid); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_Grid.size() * sizeof(int), m_Grid.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, m_SSBO_grid); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); } GameOfLife::~GameOfLife() @@ -91,7 +102,7 @@ void GameOfLife::OnCompute() void GameOfLife::OnRender(Timestep ts) { m_CameraController.OnUpdate(ts); - glClearColor(0.5f, 0.5f, 0.5f, 1.0f); + glClearColor(m_ClearColor.r, m_ClearColor.g, m_ClearColor.b, m_ClearColor.a); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(m_Shader->GetRendererID()); @@ -102,7 +113,29 @@ void GameOfLife::OnRender(Timestep ts) location = glGetUniformLocation(m_Shader->GetRendererID(), "u_ViewProjection"); glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(m_CameraController.GetCamera().GetViewProjectionMatrix())); + glUseProgram(m_GridShader->GetRendererID()); + + location = glGetUniformLocation(m_GridShader->GetRendererID(), "u_GridSize"); + glUniform1i(location, m_GridSize); + + location = glGetUniformLocation(m_GridShader->GetRendererID(), "u_ViewProjection"); + glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(m_CameraController.GetCamera().GetViewProjectionMatrix())); + + location = glGetUniformLocation(m_GridShader->GetRendererID(), "u_ClearColor"); + glUniform4fv(location, 1, glm::value_ptr(m_ClearColor)); + + glUseProgram(m_Shader->GetRendererID()); + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr, m_GridSize * m_GridSize); + + + if (m_ShowGrid) + { + glUseProgram(m_GridShader->GetRendererID()); + + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr, m_GridSize * m_GridSize); + } + } void GameOfLife::OnEvent(Event& event) @@ -123,9 +156,21 @@ void GameOfLife::RandomGrid() } + +void GameOfLife::FillGrid() +{ + for (int i = 0; i < m_GridSize * m_GridSize; i++) + { + m_Grid.at(i) = 1; + } +} + + void GameOfLife::AddCell(int xcoord, int ycoord) { int index = GetCellIndex(xcoord, ycoord, this); + + std::cout << "Index: " << index << "\n"; m_ParticleStates_in.at(index) = 1; glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); @@ -133,12 +178,27 @@ void GameOfLife::AddCell(int xcoord, int ycoord) glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); } +void GameOfLife::RemoveCell(int xcoord, int ycoord) +{ + int index = GetCellIndex(xcoord, ycoord, this); + + std::cout << "Index: " << index << "\n"; + m_ParticleStates_in.at(index) = 0; + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates_in.size() * sizeof(int), m_ParticleStates_in.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); +} + int GetCellIndex(int xcoord, int ycoord, GameOfLife* game) { - int gridsize = game->m_GridSize; + float gridsize = game->m_GridSize; + + float cellwidth = 1000.0f / gridsize; - glm::vec2 CellCoord = glm::vec2(xcoord / gridsize, (ycoord - 1000)/ gridsize); + glm::vec2 CellCoord = glm::vec2(floor((xcoord) / cellwidth), floor((std::abs(ycoord - 1000)) / cellwidth)); + std::cout << "Coord x" << CellCoord.x << " y" << CellCoord.y << "\n"; return (CellCoord.y) * gridsize + (CellCoord.x); } diff --git a/OpenGL-Sandbox/src/GameOfLife.h b/OpenGL-Sandbox/src/GameOfLife.h index d5f4e846..d1f0097b 100644 --- a/OpenGL-Sandbox/src/GameOfLife.h +++ b/OpenGL-Sandbox/src/GameOfLife.h @@ -25,6 +25,9 @@ class GameOfLife void OnEvent(Event& e); void RandomGrid(); void AddCell(int xcoord, int ycoord); + void RemoveCell(int xcoord, int ycoord); + void FillGrid(); + void ToggleGrid() { m_ShowGrid = m_ShowGrid ? false : true; }; private: int m_GridSize; @@ -33,12 +36,18 @@ class GameOfLife std::vector m_ParticleStates_in; std::vector m_ParticleStates_out; + std::vector m_Grid; + + //Conditions + bool m_ShowGrid = true; //Drawing Stuff GLuint m_VBO, m_VAO, m_EBO; - GLuint m_SSBO_in, m_SSBO_out; + GLuint m_SSBO_in, m_SSBO_out, m_SSBO_grid; GLuint m_CompShader; GLCore::Utils::Shader* m_Shader; + GLCore::Utils::Shader* m_GridShader; + glm::vec4 m_ClearColor = glm::vec4(0.5f, 0.5f, 0.5f, 1.0f); }; diff --git a/OpenGL-Sandbox/src/GridLayer.cpp b/OpenGL-Sandbox/src/GridLayer.cpp new file mode 100644 index 00000000..e3f9fe50 --- /dev/null +++ b/OpenGL-Sandbox/src/GridLayer.cpp @@ -0,0 +1 @@ +#include "GridLayer.h" diff --git a/OpenGL-Sandbox/src/GridLayer.h b/OpenGL-Sandbox/src/GridLayer.h new file mode 100644 index 00000000..4b39285b --- /dev/null +++ b/OpenGL-Sandbox/src/GridLayer.h @@ -0,0 +1,7 @@ +#pragma once +#include "GlCore/Core/Layer.h" +class GridLayer : public GLCore::Layer +{ + +}; + diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index 7e397b2a..9bf1da50 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -48,7 +48,7 @@ void SandboxLayer::OnEvent(Event& event) m_XMousePos = e.GetX(); m_YMousePos = e.GetY(); - std::cout << m_XMousePos << " " << m_YMousePos << "\n"; + return false; }); @@ -59,10 +59,19 @@ void SandboxLayer::OnEvent(Event& event) { m_Game->AddCell(m_XMousePos, m_YMousePos); - } return false; }); + + dispatcher.Dispatch([&](MouseButtonPressedEvent& e) + { + if (e.GetMouseButton() == LeftClick) + { + m_Game->RemoveCell(m_XMousePos, m_YMousePos); + + } + return false; + }); } @@ -98,6 +107,9 @@ void SandboxLayer::OnImGuiRender() m_Game = ChangeGridSize(m_Game, m_GridSize); } - + if (ImGui::Button("Show Grid")) + { + m_Game->ToggleGrid(); + } } From e4ac07ea4d076b488c1e4d3256e079fb5edae651 Mon Sep 17 00:00:00 2001 From: Karl Riedewald Date: Wed, 31 Dec 2025 23:29:24 +0000 Subject: [PATCH 10/11] Simple Ising Model Works --- OpenGL-Core/src/GLCore/Core/Application.cpp | 9 +- OpenGL-Core/src/GLCore/ImGui/ImGuiLayer.cpp | 2 +- OpenGL-Sandbox/Shaders/CellRender.frag.glsl | 25 +- OpenGL-Sandbox/Shaders/CellRender.vert.glsl | 14 +- OpenGL-Sandbox/Shaders/GridRender.frag.glsl | 6 +- OpenGL-Sandbox/Shaders/GridRender.vert.glsl | 2 - OpenGL-Sandbox/Shaders/IslingModel.glsl | 93 +++++ OpenGL-Sandbox/Shaders/LiquidGas.comp.glsl | 111 ++++++ OpenGL-Sandbox/src/GameOfLife.cpp | 169 +++++++- OpenGL-Sandbox/src/GameOfLife.h | 39 +- OpenGL-Sandbox/src/GridLayer.cpp | 1 - OpenGL-Sandbox/src/GridLayer.h | 7 - OpenGL-Sandbox/src/LiquidGas.cpp | 408 ++++++++++++++++++++ OpenGL-Sandbox/src/LiquidGas.h | 85 ++++ OpenGL-Sandbox/src/SandboxApp.cpp | 2 +- OpenGL-Sandbox/src/SandboxLayer.cpp | 197 +++++++--- OpenGL-Sandbox/src/SandboxLayer.h | 27 +- 17 files changed, 1088 insertions(+), 109 deletions(-) create mode 100644 OpenGL-Sandbox/Shaders/IslingModel.glsl create mode 100644 OpenGL-Sandbox/Shaders/LiquidGas.comp.glsl delete mode 100644 OpenGL-Sandbox/src/GridLayer.cpp delete mode 100644 OpenGL-Sandbox/src/GridLayer.h create mode 100644 OpenGL-Sandbox/src/LiquidGas.cpp create mode 100644 OpenGL-Sandbox/src/LiquidGas.h diff --git a/OpenGL-Core/src/GLCore/Core/Application.cpp b/OpenGL-Core/src/GLCore/Core/Application.cpp index 432d8625..e8b16509 100644 --- a/OpenGL-Core/src/GLCore/Core/Application.cpp +++ b/OpenGL-Core/src/GLCore/Core/Application.cpp @@ -58,7 +58,7 @@ namespace GLCore { return false; } }); - dispatcher.Dispatch(BIND_EVENT_FN(OnWindowResize)); + for (auto it = m_LayerStack.end(); it != m_LayerStack.begin(); ) { @@ -97,11 +97,6 @@ namespace GLCore { return true; } - bool Application::OnWindowResize(WindowResizeEvent& e) - { - - glViewport(0, 0, e.GetWidth(), e.GetHeight()); - return false; - } + } \ No newline at end of file diff --git a/OpenGL-Core/src/GLCore/ImGui/ImGuiLayer.cpp b/OpenGL-Core/src/GLCore/ImGui/ImGuiLayer.cpp index 729987f0..ae096182 100644 --- a/OpenGL-Core/src/GLCore/ImGui/ImGuiLayer.cpp +++ b/OpenGL-Core/src/GLCore/ImGui/ImGuiLayer.cpp @@ -27,7 +27,7 @@ namespace GLCore { io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows - + io.WantCaptureMouse = false; // Setup Dear ImGui style ImGui::StyleColorsDark(); diff --git a/OpenGL-Sandbox/Shaders/CellRender.frag.glsl b/OpenGL-Sandbox/Shaders/CellRender.frag.glsl index 06c0eafd..6e0970da 100644 --- a/OpenGL-Sandbox/Shaders/CellRender.frag.glsl +++ b/OpenGL-Sandbox/Shaders/CellRender.frag.glsl @@ -1,16 +1,21 @@ #version 440 core + + in vec2 v_LocalPosition; out vec4 FragColor; float BorderWidth = 0.0f; +flat in int State; +flat in int Debug; + + void main() { - FragColor = vec4(0.08, 0.33, 0.78, 1.0); - + FragColor = vec4(0.5, 0.5, 0.5, 1.0); //set border if (v_LocalPosition.x < -1 + BorderWidth || v_LocalPosition.x > 1 - BorderWidth || v_LocalPosition.y < -1 + BorderWidth || v_LocalPosition.y > 1 - BorderWidth) @@ -18,4 +23,20 @@ void main() FragColor = vec4(0.0, 0.0, 0.0, 1.0); } + if (State == 1) + { + FragColor = vec4(0.0, 0.0, 1.0, 1.0); + } + + if (Debug == 1) + { + FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + if (Debug == 2) + { + FragColor = vec4(0.0, 1.0, 0.0, 1.0); + } + + + } \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/CellRender.vert.glsl b/OpenGL-Sandbox/Shaders/CellRender.vert.glsl index a997bb59..9f42666c 100644 --- a/OpenGL-Sandbox/Shaders/CellRender.vert.glsl +++ b/OpenGL-Sandbox/Shaders/CellRender.vert.glsl @@ -8,6 +8,11 @@ layout(std430, binding = 1) buffer layoutName int data_SSBO[]; }; +layout(std430, binding = 4) buffer debugsdf +{ + int data_debug[]; +}; + out vec2 v_LocalPosition; uniform int u_GridSize; @@ -18,11 +23,11 @@ int i = int(gl_InstanceID); vec2 cell = vec2(i % u_GridSize, floor(i / u_GridSize)); -int State = data_SSBO[gl_InstanceID]; uniform mat4 u_ViewProjection; - +flat out int Debug; +flat out int State; void main() @@ -31,9 +36,10 @@ void main() v_LocalPosition = aPos; vec2 position = (aPos + 1) / u_GridSize - 1; - position = position * State + 2 * (cell / u_GridSize); + position = position + 2 * (cell / u_GridSize); - + State = data_SSBO[i]; + Debug = data_debug[i]; gl_Position = u_ViewProjection * vec4(position, 0.0f, 1.0f); diff --git a/OpenGL-Sandbox/Shaders/GridRender.frag.glsl b/OpenGL-Sandbox/Shaders/GridRender.frag.glsl index 694c80cf..f4861cf2 100644 --- a/OpenGL-Sandbox/Shaders/GridRender.frag.glsl +++ b/OpenGL-Sandbox/Shaders/GridRender.frag.glsl @@ -4,7 +4,7 @@ in vec2 v_LocalPosition; out vec4 FragColor; -float BorderWidth = 0.1f; +uniform float u_BorderThickness; uniform vec4 u_ClearColor; @@ -14,8 +14,8 @@ void main() FragColor = u_ClearColor; //set border - if (v_LocalPosition.x < -1 + BorderWidth || v_LocalPosition.x > 1 - BorderWidth || - v_LocalPosition.y < -1 + BorderWidth || v_LocalPosition.y > 1 - BorderWidth) + if (v_LocalPosition.x < -1 + u_BorderThickness || v_LocalPosition.x > 1 - u_BorderThickness || + v_LocalPosition.y < -1 + u_BorderThickness || v_LocalPosition.y > 1 - u_BorderThickness) { FragColor = vec4(0.0, 0.0, 0.0, 1.0); } diff --git a/OpenGL-Sandbox/Shaders/GridRender.vert.glsl b/OpenGL-Sandbox/Shaders/GridRender.vert.glsl index 0f59ebfc..7ed44a52 100644 --- a/OpenGL-Sandbox/Shaders/GridRender.vert.glsl +++ b/OpenGL-Sandbox/Shaders/GridRender.vert.glsl @@ -23,8 +23,6 @@ int State = data_SSBO[gl_InstanceID]; uniform mat4 u_ViewProjection; - - void main() { diff --git a/OpenGL-Sandbox/Shaders/IslingModel.glsl b/OpenGL-Sandbox/Shaders/IslingModel.glsl new file mode 100644 index 00000000..84a0e6c0 --- /dev/null +++ b/OpenGL-Sandbox/Shaders/IslingModel.glsl @@ -0,0 +1,93 @@ +#version 430 + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(binding = 1, std430) readonly buffer ssbo_in +{ + int State_in[]; +}; + +layout(binding = 2, std430) buffer ssbo_out +{ + int State_out[]; +}; + +uniform int u_GridSize; +uniform float u_Temperature; +uniform float u_ChemicalPotential; +uniform float u_Time; +uniform int u_Step; + +int CellIndex(vec2 CellCoord) +{ + return int(CellCoord.y) * u_GridSize + int(CellCoord.x); +} + +int SpinState(int x, int y) +{ + int array_value = int(State_in[CellIndex(vec2(x,y))]); + + return array_value == 1 ? 1 : -1; +} + + +uint hash(uint x) +{ + x ^= x >> 16; + x *= 0x7feb352d; + x ^= x >> 15; + x *= 0x846ca68b; + x ^= x >> 16; + return x; +} + +float rand(uint seed) +{ + return float(hash(seed)) / float(0xffffffffu); +} + +void main() +{ + +vec2 cell = gl_GlobalInvocationID.xy; + +int i = CellIndex(cell); + +//float random = rand(dot(cell, vec2(12.9898, 78.233)) + u_Time); +//float random2 = rand(dot(cell, vec2(12.9898, 78.233)) + u_Time); + + +uint seed = uint(cell.x) + uint(cell.y) * uint(u_GridSize) + u_Step * 1664525u; + +float random = rand(seed); + + + +int energy_surrouding = SpinState(int(cell.x+1), int(cell.y+1)) + + SpinState(int(cell.x+1), int(cell.y)) + + SpinState(int(cell.x+1), int(cell.y-1)) + + SpinState(int(cell.x), int(cell.y-1)) + + SpinState(int(cell.x-1), int(cell.y-1)) + + SpinState(int(cell.x-1), int(cell.y)) + + SpinState(int(cell.x-1), int(cell.y+1)) + + SpinState(int(cell.x), int(cell.y+1)); + +float energy = 2.0f * float(SpinState(int(cell.x), int(cell.y))) * float(energy_surrouding); + +float q = exp(energy / u_Temperature); + +float probability = 1.0f / (1.0f + exp(float(energy) / float(u_Temperature))); + + +if (random < probability ) +{ + State_out[i] = 1 - State_in[i]; +} + +else +{ + State_out[i] = State_in[i]; +} + + +} \ No newline at end of file diff --git a/OpenGL-Sandbox/Shaders/LiquidGas.comp.glsl b/OpenGL-Sandbox/Shaders/LiquidGas.comp.glsl new file mode 100644 index 00000000..9b12f995 --- /dev/null +++ b/OpenGL-Sandbox/Shaders/LiquidGas.comp.glsl @@ -0,0 +1,111 @@ +#version 430 + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(binding = 1, std430) readonly buffer ssbo_in +{ + int State_in[]; +}; + +layout(binding = 2, std430) buffer ssbo_out +{ + int State_out[]; +}; + +uniform int u_GridSize; +uniform float u_Temperature; +uniform float u_ChemicalPotential; +uniform float u_Time; +uniform int u_Step; + +int CellIndex(vec2 CellCoord) +{ + return int(CellCoord.y) * u_GridSize + int(CellCoord.x); +} + +int SpinState(int x, int y) +{ + int array_value = int(State_in[CellIndex(vec2(x,y))]); + + return array_value == 1 ? 1 : -1; +} + +int ParticlePresent(int x, int y) +{ + return int(State_in[CellIndex(vec2(x,y))]); +} + + +uint hash(uint x) +{ + x ^= x >> 16; + x *= 0x7feb352d; + x ^= x >> 15; + x *= 0x846ca68b; + x ^= x >> 16; + return x; +} + +float rand(uint seed) +{ + return float(hash(seed)) / float(0xffffffffu); +} + +void main() +{ + +vec2 cell = gl_GlobalInvocationID.xy; + +int i = CellIndex(cell); + +//float random = rand(dot(cell, vec2(12.9898, 78.233)) + u_Time); +//float random2 = rand(dot(cell, vec2(12.9898, 78.233)) + u_Time); + + +uint seed = uint(cell.x) + uint(cell.y) * uint(u_GridSize) + u_Step * 1664525u; + +float random = rand(seed); + + + +int energy_surrouding = SpinState(int(cell.x+1), int(cell.y+1)) + + SpinState(int(cell.x+1), int(cell.y)) + + SpinState(int(cell.x+1), int(cell.y-1)) + + SpinState(int(cell.x), int(cell.y-1)) + + SpinState(int(cell.x-1), int(cell.y-1)) + + SpinState(int(cell.x-1), int(cell.y)) + + SpinState(int(cell.x-1), int(cell.y+1)) + + SpinState(int(cell.x), int(cell.y+1)); + +int particles_surrounding = ParticlePresent(int(cell.x+1), int(cell.y+1)) + + ParticlePresent(int(cell.x+1), int(cell.y)) + + ParticlePresent(int(cell.x+1), int(cell.y-1)) + + ParticlePresent(int(cell.x), int(cell.y-1)) + + ParticlePresent(int(cell.x-1), int(cell.y-1)) + + ParticlePresent(int(cell.x-1), int(cell.y)) + + ParticlePresent(int(cell.x-1), int(cell.y+1)) + + ParticlePresent(int(cell.x), int(cell.y+1)); + +float energy = 2.0f * float(SpinState(int(cell.x), int(cell.y))) * float(energy_surrouding); + +//float q = exp((energy - u_ChemicalPotential * float(particles_surrounding)) / u_Temperature); + +//float probability = 1.0f / (1.0f + exp((float(energy - u_ChemicalPotential * float(particles_surrounding))) / float(u_Temperature))); +float probability = 1.0f / (1.0f + exp((float(energy)) / float(u_Temperature))); + + +if (random < probability ) +{ + State_out[i] = 1 - State_in[i]; +} + +else +{ + State_out[i] = State_in[i]; +} + + + + + +} \ No newline at end of file diff --git a/OpenGL-Sandbox/src/GameOfLife.cpp b/OpenGL-Sandbox/src/GameOfLife.cpp index 953b4dc6..0b1a4a13 100644 --- a/OpenGL-Sandbox/src/GameOfLife.cpp +++ b/OpenGL-Sandbox/src/GameOfLife.cpp @@ -1,4 +1,6 @@ #include "GameOfLife.h" +#include + using namespace GLCore; using namespace GLCore::Utils; @@ -13,8 +15,14 @@ GameOfLife::GameOfLife(int GridSize) m_GridShader = Shader::FromGLSLTextFiles("Shaders/GridRender.vert.glsl", "Shaders/GridRender.frag.glsl"); - m_CompShader = CreateComputeShader("Shaders/GameOfLife.comp.glsl"); + m_CompShaderGameOfLife = CreateComputeShader("Shaders/GameOfLife.comp.glsl"); + m_CompShaderLiquidGas = CreateComputeShader("Shaders/LiquidGas.comp.glsl"); + if (m_ShouldCreateFramebuffer) + { + CreateFramebufferWithTexture(); + m_ShouldCreateFramebuffer = false; + } FillGrid(); RandomGrid(); @@ -71,24 +79,61 @@ GameOfLife::GameOfLife(int GridSize) GameOfLife::~GameOfLife() { - GLuint buffers[] = { m_VBO, m_EBO, m_SSBO_in, m_SSBO_out }; + GLuint buffers[] = { m_VBO, m_EBO, m_SSBO_in, m_SSBO_out, m_SSBO_grid }; glDeleteBuffers(IM_ARRAYSIZE(buffers), buffers); glDeleteVertexArrays(1, &m_VAO); glDeleteProgram(m_Shader->GetRendererID()); - glDeleteProgram(m_CompShader); + glDeleteProgram(m_CompShaderGameOfLife); + glDeleteProgram(m_CompShaderLiquidGas); + + glDeleteFramebuffers(1, &m_FBO); + +} + +void GameOfLife::OnComputeGameOfLife() +{ + glUseProgram(m_CompShaderGameOfLife); + + int location = glGetUniformLocation(m_CompShaderGameOfLife, "u_GridSize"); + glUniform1i(location, m_GridSize); + + glDispatchCompute(m_GridSize / 8, m_GridSize / 8, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); + std::swap(m_SSBO_in, m_SSBO_out); + + glUseProgram(0); } -void GameOfLife::OnCompute() +void GameOfLife::OnComputeLiquidGas() { - glUseProgram(m_CompShader); + glUseProgram(m_CompShaderLiquidGas); - int location = glGetUniformLocation(m_CompShader, "u_GridSize"); + int location = glGetUniformLocation(m_CompShaderLiquidGas, "u_GridSize"); glUniform1i(location, m_GridSize); + location = glGetUniformLocation(m_CompShaderLiquidGas, "u_Temperature"); + glUniform1f(location, m_Temperature); + + location = glGetUniformLocation(m_CompShaderLiquidGas, "u_ChemicalPotential"); + glUniform1f(location, m_ChemcialPotential); + + static int step = 0; + location = glGetUniformLocation(m_CompShaderLiquidGas, "u_Step"); + glUniform1i(location, step); + step++; + + auto time = float(glfwGetTime()); + + location = glGetUniformLocation(m_CompShaderLiquidGas, "u_Time"); + glUniform1f(location, time); + glDispatchCompute(m_GridSize / 8, m_GridSize / 8, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); @@ -102,6 +147,9 @@ void GameOfLife::OnCompute() void GameOfLife::OnRender(Timestep ts) { m_CameraController.OnUpdate(ts); + glBindFramebuffer(GL_FRAMEBUFFER, m_FBO); + glViewport(0, 0, (int)m_FBTextureSize.x, (int)m_FBTextureSize.y); + glClearColor(m_ClearColor.r, m_ClearColor.g, m_ClearColor.b, m_ClearColor.a); glClear(GL_COLOR_BUFFER_BIT); @@ -124,6 +172,9 @@ void GameOfLife::OnRender(Timestep ts) location = glGetUniformLocation(m_GridShader->GetRendererID(), "u_ClearColor"); glUniform4fv(location, 1, glm::value_ptr(m_ClearColor)); + location = glGetUniformLocation(m_GridShader->GetRendererID(), "u_BorderThickness"); + glUniform1f(location, m_BorderThickness); + glUseProgram(m_Shader->GetRendererID()); glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr, m_GridSize * m_GridSize); @@ -135,19 +186,46 @@ void GameOfLife::OnRender(Timestep ts) glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr, m_GridSize * m_GridSize); } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); } +void GameOfLife::CreateFramebufferWithTexture() +{ + glGenFramebuffers(1, &m_FBO); + glBindFramebuffer(GL_FRAMEBUFFER, m_FBO); + + glGenTextures(1, &m_FBTexture); + glBindTexture(GL_TEXTURE_2D, m_FBTexture); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, int(m_FBTextureSize.x), int(m_FBTextureSize.y), 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_FBTexture, 0); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + LOG_WARN("Framebuffer Failed to Complete!"); + LOG_WARN(glCheckFramebufferStatus(GL_FRAMEBUFFER)); + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + +} + void GameOfLife::OnEvent(Event& event) { - m_CameraController.OnEvent(event); + //m_CameraController.OnEvent(event); } void GameOfLife::RandomGrid() { for (int i = 0; i < m_GridSize * m_GridSize; i++) { - int choices = 5; + int choices = 2; int picked_choice = (int)(rand() % choices); if (picked_choice == 1) { m_ParticleStates_in.at(i) = 1; @@ -155,8 +233,6 @@ void GameOfLife::RandomGrid() } } - - void GameOfLife::FillGrid() { for (int i = 0; i < m_GridSize * m_GridSize; i++) @@ -166,6 +242,26 @@ void GameOfLife::FillGrid() } +void GameOfLife::ReloadFramebuffer() +{ + glDeleteFramebuffers(1, &m_FBO); + + glDeleteTextures(1, &m_FBTexture); + + this->CreateFramebufferWithTexture(); +} + +void GameOfLife::SetGridBorderThickness(float thickness) +{ + m_BorderThickness = thickness; +} + + +GLuint GameOfLife::GetFBTextureID() +{ + return m_FBTexture; +} + void GameOfLife::AddCell(int xcoord, int ycoord) { int index = GetCellIndex(xcoord, ycoord, this); @@ -203,12 +299,57 @@ int GetCellIndex(int xcoord, int ycoord, GameOfLife* game) return (CellCoord.y) * gridsize + (CellCoord.x); } -GameOfLife* ChangeGridSize(GameOfLife* game, int newGridSize) +void GameOfLife::ChangeGridSize(int newGridSize) { - delete game; + GLuint buffers[] = { m_SSBO_in, m_SSBO_out, m_SSBO_grid }; + glDeleteBuffers(2, buffers); + + + m_ParticleStates_in.resize(newGridSize * newGridSize); + m_ParticleStates_out.resize(newGridSize * newGridSize); + m_Grid.resize(newGridSize * newGridSize); + + std::fill(m_ParticleStates_in.begin(), m_ParticleStates_in.end(), 0); + std::fill(m_ParticleStates_out.begin(), m_ParticleStates_out.end(), 0); + std::fill(m_Grid.begin(), m_Grid.end(), 0); + + m_GridSize = newGridSize; + + + FillGrid(); + RandomGrid(); + + //create SSBO Buffer + glGenBuffers(1, &m_SSBO_in); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates_in.size() * sizeof(int), m_ParticleStates_in.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); - GameOfLife* newGame = new GameOfLife(newGridSize); + glGenBuffers(1, &m_SSBO_out); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_out); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates_out.size() * sizeof(int), m_ParticleStates_out.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); - return newGame; + glGenBuffers(1, &m_SSBO_grid); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_grid); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_Grid.size() * sizeof(int), m_Grid.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, m_SSBO_grid); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); } + +void GameOfLife::ResetSimulation() +{ + std::fill(m_ParticleStates_in.begin(), m_ParticleStates_in.end(), 0); + std::fill(m_ParticleStates_out.begin(), m_ParticleStates_out.end(), 0); + + RandomGrid(); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_in); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates_in.size() * sizeof(int), m_ParticleStates_in.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_in); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_out); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates_out.size() * sizeof(int), m_ParticleStates_out.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_SSBO_out); +} diff --git a/OpenGL-Sandbox/src/GameOfLife.h b/OpenGL-Sandbox/src/GameOfLife.h index d1f0097b..b5d21dc7 100644 --- a/OpenGL-Sandbox/src/GameOfLife.h +++ b/OpenGL-Sandbox/src/GameOfLife.h @@ -2,7 +2,7 @@ #include #include #include -#include "GLCore/Util/Texture.h" +#include "GLCore/Core/Log.h" #include "GLCore/Util/Shader.h" #define GRID_SIZE 100 @@ -12,6 +12,8 @@ using namespace GLCore::Utils; + + class GameOfLife { friend int GetCellIndex(int xcoord, int ycoord, GameOfLife* game); @@ -20,35 +22,62 @@ class GameOfLife GameOfLife(int GridSize); ~GameOfLife(); - void OnCompute(); + void OnComputeGameOfLife(); + void OnComputeLiquidGas(); void OnRender(Timestep ts); + void CreateFramebufferWithTexture(); void OnEvent(Event& e); void RandomGrid(); void AddCell(int xcoord, int ycoord); void RemoveCell(int xcoord, int ycoord); void FillGrid(); void ToggleGrid() { m_ShowGrid = m_ShowGrid ? false : true; }; + void ChangeGridSize(int newsize); + void ResetSimulation(); -private: + void ReloadFramebuffer(); + + //Setters + void SetGridBorderThickness(float thickness); + + + //Getters + GLuint GetFBTextureID(); + int m_GridSize; + glm::vec2 m_FBTextureSize = glm::vec2(500.0f, 500.0f); GLCore::Utils::OrthographicCameraController m_CameraController; +private: + std::vector m_ParticleStates_in; std::vector m_ParticleStates_out; std::vector m_Grid; //Conditions - bool m_ShowGrid = true; + bool m_ShowGrid = false; + bool m_ShouldCreateFramebuffer = true; + float m_BorderThickness = 0.1f; + //Drawing Stuff GLuint m_VBO, m_VAO, m_EBO; GLuint m_SSBO_in, m_SSBO_out, m_SSBO_grid; - GLuint m_CompShader; + GLuint m_CompShaderGameOfLife, m_CompShaderLiquidGas; GLCore::Utils::Shader* m_Shader; GLCore::Utils::Shader* m_GridShader; glm::vec4 m_ClearColor = glm::vec4(0.5f, 0.5f, 0.5f, 1.0f); + //Framebuffer + GLuint m_FBO; + GLuint m_FBTexture; + + //Liquid Gas Comp Shader Variables +public: + float m_Temperature = 0.0f; + float m_ChemcialPotential = 0.0f; + }; diff --git a/OpenGL-Sandbox/src/GridLayer.cpp b/OpenGL-Sandbox/src/GridLayer.cpp deleted file mode 100644 index e3f9fe50..00000000 --- a/OpenGL-Sandbox/src/GridLayer.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "GridLayer.h" diff --git a/OpenGL-Sandbox/src/GridLayer.h b/OpenGL-Sandbox/src/GridLayer.h deleted file mode 100644 index 4b39285b..00000000 --- a/OpenGL-Sandbox/src/GridLayer.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -#include "GlCore/Core/Layer.h" -class GridLayer : public GLCore::Layer -{ - -}; - diff --git a/OpenGL-Sandbox/src/LiquidGas.cpp b/OpenGL-Sandbox/src/LiquidGas.cpp new file mode 100644 index 00000000..9209bcbe --- /dev/null +++ b/OpenGL-Sandbox/src/LiquidGas.cpp @@ -0,0 +1,408 @@ +#include "LiquidGas.h" + + +using namespace GLCore; +using namespace GLCore::Utils; + +#include +#include +#include +#include + + + + + +LiquidGas::LiquidGas(int GridSize) + :m_CameraController(1.0f), m_GridSize(GridSize), + m_Grid(GridSize* GridSize), + m_ParticleStates(GridSize * GridSize), + m_Debug(GridSize * GridSize), + m_Distribution(0, GridSize - 1) +{ + m_Shader = Shader::FromGLSLTextFiles("Shaders/CellRender.vert.glsl", "Shaders/CellRender.frag.glsl"); + + m_GridShader = Shader::FromGLSLTextFiles("Shaders/GridRender.vert.glsl", "Shaders/GridRender.frag.glsl"); + + if (m_ShouldCreateFramebuffer) + { + CreateFramebufferWithTexture(); + m_ShouldCreateFramebuffer = false; + } + + FillGrid(); + RandomParticlesFill(); + + float vertices[3 * 4] = { + // positions + 1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 0.0f, + -1.0f, -1.0f, 0.0f, + -1.0f, 1.0f, 0.0f + }; + + glGenVertexArrays(1, &m_VAO); + glGenBuffers(1, &m_VBO); + glGenBuffers(1, &m_EBO); + + glBindVertexArray(m_VAO); + + glBindBuffer(GL_ARRAY_BUFFER, m_VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + unsigned int indices[3 * 3] = { + 0, 1, 3, // first triangle + 1, 2, 3 // second triangle + }; + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // position attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + glGenBuffers(1, &m_SSBO_particles); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_particles); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates.size() * sizeof(int), m_ParticleStates.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_particles); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + glGenBuffers(1, &m_SSBO_grid); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_grid); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_Grid.size() * sizeof(int), m_Grid.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, m_SSBO_grid); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + glGenBuffers(1, &m_SSBO_debug); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_debug); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_Debug.size() * sizeof(int), m_Debug.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, m_SSBO_debug); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); +} + +LiquidGas::~LiquidGas() +{ + GLuint buffers[] = { m_VBO, m_EBO, m_SSBO_grid, m_SSBO_particles}; + glDeleteBuffers(IM_ARRAYSIZE(buffers), buffers); + + glDeleteVertexArrays(1, &m_VAO); + + glDeleteProgram(m_Shader->GetRendererID()); + + glDeleteFramebuffers(1, &m_FBO); + +} + +int LiquidGas::CellIndex(int x, int y) +{ + if (std::sqrt(m_ParticleStates.size()) != int(std::sqrt(m_ParticleStates.size()))) + LOG_WARN("NOT A SQUARE NUMBER! CELL INDEX ERROR"); + + + int gridheight = m_GridSize; + return (y * gridheight + x); +} + +int LiquidGas::CellActive(int x, int y) +{ + + //out of bounds check + if (x > m_GridSize - 1 || x < 0 || + y > m_GridSize - 1 || y < 0) + { + return 0; + } + + int state = m_ParticleStates[this->CellIndex(x, y)]; + if (state == 0 || state == 1) + return state; + + else + { + LOG_WARN("CELL STATE NOT 0 or 1 !!!!!!"); + return state; + } +} + +std::random_device rd; +std::mt19937 gen(rd()); +std::uniform_real_distribution dist(0.0, 1.0); + +#define BIT(x) 1 << x +enum EdgeCase +{ + NONE = BIT(0), + + MAX_X_1 = BIT(1), + MAX_X_2 = BIT(2), + + MIN_X_1 = BIT(3), + MIN_X_2 = BIT(4), + + MAX_Y_1 = BIT(5), + MAX_Y_2 = BIT(6), + + MIN_Y_1 = BIT(7), + MIN_Y_2 = BIT(8) +}; + +struct Particles +{ + glm::ivec2 a; + glm::ivec2 b; + int max; +}; + +int GetEdgeCase(Particles& p) +{ + int edgecase = NONE; + + if (p.a.x == p.max) + edgecase |= MAX_X_1; + + if (p.b.x == p.max) + edgecase |= MAX_X_2; + + if (p.a.x == 0) + edgecase |= MIN_X_1; + + if (p.b.x == 0) + edgecase |= MIN_X_2; + + if (p.a.y == p.max) + edgecase |= MAX_Y_1; + + if (p.b.y == p.max) + edgecase |= MAX_Y_2; + + if (p.a.y == 0) + edgecase |= MIN_Y_1; + + if (p.b.y == 0) + edgecase |= MIN_Y_2; + + return edgecase; +} + + +constexpr double kB = 1.380649e-23; +void LiquidGas::OnCompute() +{ + + + int max_xy = m_GridSize - 1; + for (int i = 0; i < m_GridSize * m_GridSize; i++) + { + glm::ivec2 particle_1{ m_Distribution(m_Generator), m_Distribution(m_Generator) }; + glm::ivec2 particle_2{ m_Distribution(m_Generator), m_Distribution(m_Generator) }; + + + + //do nothing if equal + if (CellActive(particle_1.x, particle_1.y) == CellActive(particle_2.x, particle_2.y)) + return; + + int active_neighbours_1 = -1, active_neighbours_2 = -1; + + active_neighbours_1 = CellActive((particle_1.x + 1), (particle_1.y + 1)) + + CellActive((particle_1.x + 1), (particle_1.y)) + + CellActive((particle_1.x + 1), (particle_1.y - 1)) + + CellActive((particle_1.x), (particle_1.y - 1)) + + CellActive((particle_1.x - 1), (particle_1.y - 1)) + + CellActive((particle_1.x - 1), (particle_1.y)) + + CellActive((particle_1.x - 1), (particle_1.y + 1)) + + CellActive((particle_1.x), (particle_1.y + 1)); + + active_neighbours_2 = CellActive((particle_2.x + 1), (particle_2.y + 1)) + + CellActive((particle_2.x + 1), (particle_2.y)) + + CellActive((particle_2.x + 1), (particle_2.y - 1)) + + CellActive((particle_2.x), (particle_2.y - 1)) + + CellActive((particle_2.x - 1), (particle_2.y - 1)) + + CellActive((particle_2.x - 1), (particle_2.y)) + + CellActive((particle_2.x - 1), (particle_2.y + 1)) + + CellActive((particle_2.x), (particle_2.y + 1)); + + + + int energy_diff = std::abs(active_neighbours_1 - active_neighbours_2); + + + double q = std::exp( -energy_diff / (m_Temperature)); + + double swap_probability = (q) / (1.0 + q); + + int particle_1_index = CellIndex(particle_1.x, particle_1.y); + int particle_2_index = CellIndex(particle_2.x, particle_2.y); + + if (dist(gen) < swap_probability) + std::swap(m_ParticleStates[particle_1_index], m_ParticleStates[particle_2_index]); + } + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_particles); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates.size() * sizeof(int), m_ParticleStates.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_particles); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_debug); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_Debug.size() * sizeof(int), m_Debug.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, m_SSBO_debug); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + +} + +void LiquidGas::OnRender(Timestep ts) +{ + m_CameraController.OnUpdate(ts); + glBindFramebuffer(GL_FRAMEBUFFER, m_FBO); + glViewport(0, 0, (int)m_FBTextureSize.x, (int)m_FBTextureSize.y); + + glClearColor(m_ClearColor.r, m_ClearColor.g, m_ClearColor.b, m_ClearColor.a); + glClear(GL_COLOR_BUFFER_BIT); + + glUseProgram(m_Shader->GetRendererID()); + + int location = glGetUniformLocation(m_Shader->GetRendererID(), "u_GridSize"); + glUniform1i(location, m_GridSize); + + location = glGetUniformLocation(m_Shader->GetRendererID(), "u_ViewProjection"); + glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(m_CameraController.GetCamera().GetViewProjectionMatrix())); + + glUseProgram(m_GridShader->GetRendererID()); + + location = glGetUniformLocation(m_GridShader->GetRendererID(), "u_GridSize"); + glUniform1i(location, m_GridSize); + + location = glGetUniformLocation(m_GridShader->GetRendererID(), "u_ViewProjection"); + glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(m_CameraController.GetCamera().GetViewProjectionMatrix())); + + location = glGetUniformLocation(m_GridShader->GetRendererID(), "u_ClearColor"); + glUniform4fv(location, 1, glm::value_ptr(m_ClearColor)); + + location = glGetUniformLocation(m_GridShader->GetRendererID(), "u_BorderThickness"); + glUniform1f(location, m_BorderThickness); + + glUseProgram(m_Shader->GetRendererID()); + + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr, m_GridSize * m_GridSize); + + + if (m_ShowGrid) + { + glUseProgram(m_GridShader->GetRendererID()); + + glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr, m_GridSize * m_GridSize); + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void LiquidGas::CreateFramebufferWithTexture() +{ + glGenFramebuffers(1, &m_FBO); + glBindFramebuffer(GL_FRAMEBUFFER, m_FBO); + + glGenTextures(1, &m_FBTexture); + glBindTexture(GL_TEXTURE_2D, m_FBTexture); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, int(m_FBTextureSize.x), int(m_FBTextureSize.y), 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_FBTexture, 0); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + LOG_WARN("Framebuffer Failed to Complete!"); + LOG_WARN(glCheckFramebufferStatus(GL_FRAMEBUFFER)); + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + +} + +void LiquidGas::OnEvent(Event& event) +{ + //m_CameraController.OnEvent(event); +} + +void LiquidGas::RandomParticlesFill() +{ + for (int i = 0; i < m_GridSize * m_GridSize; i++) + { + int choices = 2; + int picked_choice = (int)(rand() % choices); + if (picked_choice == 1) { + m_ParticleStates.at(i) = 1; + } + } +} + +void LiquidGas::TestFill() +{ + for (int i = 0; i < 600; i++) + { + m_ParticleStates[i] = 1; + } +} + +void LiquidGas::FillGrid() +{ + for (int i = 0; i < m_GridSize * m_GridSize; i++) + { + m_Grid.at(i) = 1; + } +} + + +void LiquidGas::ReloadFramebuffer() +{ + glDeleteFramebuffers(1, &m_FBO); + + glDeleteTextures(1, &m_FBTexture); + + this->CreateFramebufferWithTexture(); +} + +void LiquidGas::SetGridBorderThickness(float thickness) +{ + m_BorderThickness = thickness; +} + + +GLuint LiquidGas::GetFBTextureID() +{ + return m_FBTexture; +} + + + +void LiquidGas::ChangeGridSize(int newGridSize) +{ + m_ParticleStates.resize(newGridSize * newGridSize); + m_Grid.resize(newGridSize * newGridSize); + + std::fill(m_ParticleStates.begin(), m_ParticleStates.end(), 0); + std::fill(m_Grid.begin(), m_Grid.end(), 0); + + m_GridSize = newGridSize; + + FillGrid(); + RandomParticlesFill(); + + //create SSBO Buffer + glGenBuffers(1, &m_SSBO_grid); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_grid); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_Grid.size() * sizeof(int), m_Grid.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, m_SSBO_grid); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + glGenBuffers(1, &m_SSBO_particles); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_SSBO_particles); + glBufferData(GL_SHADER_STORAGE_BUFFER, m_ParticleStates.size() * sizeof(int), m_ParticleStates.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_SSBO_grid); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + +} + diff --git a/OpenGL-Sandbox/src/LiquidGas.h b/OpenGL-Sandbox/src/LiquidGas.h new file mode 100644 index 00000000..0b283426 --- /dev/null +++ b/OpenGL-Sandbox/src/LiquidGas.h @@ -0,0 +1,85 @@ +#pragma once +#include +#include +#include +#include "GLCore/Core/Log.h" +#include "GLCore/Util/Shader.h" +#include +#include + + +using namespace GLCore; +using namespace GLCore::Utils; + + +class LiquidGas +{ + friend int GetCellIndex(int xcoord, int ycoord, LiquidGas* game); +public: + + LiquidGas(int GridSize); + ~LiquidGas(); + + void OnCompute(); + void OnRender(Timestep ts); + void CreateFramebufferWithTexture(); + void OnEvent(Event& e); + void RandomParticlesFill(); + void AddCell(int xcoord, int ycoord); + void RemoveCell(int xcoord, int ycoord); + void FillGrid(); + void ToggleGrid() { m_ShowGrid = m_ShowGrid ? false : true; }; + void ChangeGridSize(int newsize); + + int CellIndex(int x, int y); + int CellActive(int x, int y); + + void ReloadFramebuffer(); + + void TestFill(); + + //Setters + void SetGridBorderThickness(float thickness); + + + GLuint GetFBTextureID(); + + int m_GridSize; + glm::vec2 m_FBTextureSize = glm::vec2(500.0f, 500.0f); + + GLCore::Utils::OrthographicCameraController m_CameraController; + + double m_Temperature = 5; + +private: + + std::default_random_engine m_Generator; + std::uniform_int_distribution m_Distribution; + + + std::vector m_Grid; + std::vector m_ParticleStates; + std::vector m_Debug; + + //Conditions + bool m_ShowGrid = false; + bool m_ShouldCreateFramebuffer = true; + float m_BorderThickness = 0.1f; + + + //Drawing Stuff + GLuint m_VBO, m_VAO, m_EBO; + GLuint m_SSBO_grid, m_SSBO_particles, m_SSBO_debug; + GLCore::Utils::Shader* m_Shader; + GLCore::Utils::Shader* m_GridShader; + glm::vec4 m_ClearColor = glm::vec4(0.5f, 0.5f, 0.5f, 1.0f); + + //Framebuffer + GLuint m_FBO; + GLuint m_FBTexture; +}; + +LiquidGas* ChangeGridSize(LiquidGas* game, int newGridSize); + + + diff --git a/OpenGL-Sandbox/src/SandboxApp.cpp b/OpenGL-Sandbox/src/SandboxApp.cpp index e84829bc..74b505ad 100644 --- a/OpenGL-Sandbox/src/SandboxApp.cpp +++ b/OpenGL-Sandbox/src/SandboxApp.cpp @@ -7,7 +7,7 @@ class Sandbox : public Application { public: Sandbox() - :Application("Phase Change Simulation", 1000, 1000) + :Application("Phase Change Simulation", 2560, 1440) { PushLayer(new SandboxLayer()); } diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index 9bf1da50..57de5293 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -8,85 +8,106 @@ using namespace GLCore::Utils; SandboxLayer::SandboxLayer() + :m_WindowSize(glm::vec2(2560, 1440)) { - - } SandboxLayer::~SandboxLayer() { - delete m_Game; + delete m_Game; + delete m_LiquidGas; } void SandboxLayer::OnAttach() { EnableGLDebugging(); - - - - + m_Game->m_GridSize = INIT_GRID_SIZE; } void SandboxLayer::OnDetach() { - + } void SandboxLayer::OnEvent(Event& event) { - m_Game->OnEvent(event); + m_Game->OnEvent(event); + + EventDispatcher dispatcher(event); - EventDispatcher dispatcher(event); + dispatcher.Dispatch([&](MouseMovedEvent& e) + { + m_XMousePos = e.GetX(); + m_YMousePos = e.GetY(); - dispatcher.Dispatch([&](MouseMovedEvent& e) - { - m_XMousePos = e.GetX(); - m_YMousePos = e.GetY(); - + return false; + }); - return false; - }); + /*dispatcher.Dispatch([&](MouseButtonPressedEvent& e) + { + if (e.GetMouseButton() == RightClick) + { + m_Game->AddCell(m_XMousePos, m_YMousePos); + + } + return false; + });*/ dispatcher.Dispatch([&](MouseButtonPressedEvent& e) { - if (e.GetMouseButton() == RightClick) - { - m_Game->AddCell(m_XMousePos, m_YMousePos); + if (e.GetMouseButton() == LeftClick) + { + m_Game->RemoveCell(m_XMousePos, m_YMousePos); - } - return false; + } + return false; }); - dispatcher.Dispatch([&](MouseButtonPressedEvent& e) - { - if (e.GetMouseButton() == LeftClick) - { - m_Game->RemoveCell(m_XMousePos, m_YMousePos); - } - return false; - }); - + } void SandboxLayer::OnUpdate(Timestep ts) { - - if (m_ComputeState == COMPUTE && m_TimeCounter >= m_UpdateFrequency) - { - m_Game->OnCompute(); - m_TimeCounter = 0; - } - - m_Game->OnRender(ts); - - m_TimeCounter += ts; + m_Game->m_Temperature = m_TempSlider; + m_Game->m_ChemcialPotential = m_CPSlider; + + if (m_ComputeState == COMPUTE && m_TimeCounter >= m_UpdateFrequency) + { + switch (m_WhatSimulation) + { + case GAMEOFLIFE: + { + m_Game->OnComputeGameOfLife(); + break; + } + + case LIQUIDGAS: + { + m_Game->OnComputeLiquidGas(); + break; + } + default: + break; + } + + m_TimeCounter = 0; + } + + m_Game->OnRender(ts); + + + m_TimeCounter += ts; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); } void SandboxLayer::OnRender() @@ -96,20 +117,84 @@ void SandboxLayer::OnRender() void SandboxLayer::OnImGuiRender() { - if (ImGui::Button("Start")) - { - m_ComputeState = m_ComputeState == COMPUTE ? PAUSED : COMPUTE; - } - - if (ImGui::SliderInt("GridSize", &m_GridSize, 4, 1000)) - { - m_ComputeState = PAUSED; - m_Game = ChangeGridSize(m_Game, m_GridSize); - } - - if (ImGui::Button("Show Grid")) - { - m_Game->ToggleGrid(); - } + ImGui::GetIO().FontGlobalScale = 1.5f; + ImGui::DockSpaceOverViewport(); + + ImGui::GetIO().WantCaptureMouse = false; + + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{ 0, 0 }); + + ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoResize); + ImVec2 viewportpanelsize = ImGui::GetContentRegionAvail(); + + if (m_ViewportSize != *((glm::vec2*)&viewportpanelsize)) + { + m_Game->m_FBTextureSize = { viewportpanelsize.x, viewportpanelsize.y }; + m_Game->ReloadFramebuffer(); + + m_ViewportSize = { viewportpanelsize.x, viewportpanelsize.y }; + } + + + ImGui::Image((ImTextureID)m_Game->GetFBTextureID(), viewportpanelsize); + ImGui::End(); + ImGui::PopStyleVar(); + + ImGui::Begin("Controls"); + + if (ImGui::Button("Start")) + { + m_ComputeState = m_ComputeState == COMPUTE ? PAUSED : COMPUTE; + } + + if (ImGui::Button("Reset")) + { + m_Game->ResetSimulation(); + m_ComputeState = PAUSED; + } + + + if (ImGui::SliderInt("GridSize", &m_GridSize, 4, 2000)) + { + m_ComputeState = PAUSED; + m_Game->ChangeGridSize(m_GridSize); + } + + float borderthickness = 0.1f; + + if (ImGui::SliderFloat("Grid Thickness", &borderthickness, 0.0f, 1.0f)) + { + m_Game->SetGridBorderThickness(borderthickness); + } + + ImGui::SliderFloat("Update Frequency", &m_UpdateFrequency, 0.0f, 0.1f); + + + if (ImGui::Button("Show Grid")) + { + m_Game->ToggleGrid(); + m_LiquidGas->ToggleGrid(); + } + + if (ImGui::Button("Switch Sim")) + { + m_WhatSimulation = m_WhatSimulation == GAMEOFLIFE ? LIQUIDGAS : GAMEOFLIFE; + } + + double max = 10.0; + double min = 0.0; + ImGui::SliderScalar("Temperature", ImGuiDataType_Double, &m_TempSlider, &min, &max); + + max = -1.0; + min = -3.0; + ImGui::SliderScalar("Chemical Potential", ImGuiDataType_Double, &m_CPSlider, &min, &max); + + + + + ImGui::End(); + + } diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index b1597a16..5b1749b8 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -1,8 +1,9 @@ #pragma once - - #include "GameOfLife.h" +#include "LiquidGas.h" + +#define INIT_GRID_SIZE 256 enum State { @@ -16,6 +17,12 @@ enum MouseButton LeftClick }; +enum SimulationSelector +{ + GAMEOFLIFE, + LIQUIDGAS +}; + class SandboxLayer : public GLCore::Layer { public: @@ -32,18 +39,26 @@ class SandboxLayer : public GLCore::Layer - GameOfLife* m_Game = new GameOfLife(64); + GameOfLife* m_Game = new GameOfLife(INIT_GRID_SIZE); + LiquidGas* m_LiquidGas = new LiquidGas(INIT_GRID_SIZE); State m_ComputeState = PAUSED; - float m_UpdateFrequency = 0.05f; + SimulationSelector m_WhatSimulation = LIQUIDGAS; + + float m_UpdateFrequency = 0.0f; float m_TimeCounter = 0; - int m_GridSize = 64; + int m_GridSize = INIT_GRID_SIZE; float m_XMousePos; float m_YMousePos; + //IMGUI sliders + double m_TempSlider = 2.0f; + double m_CPSlider = 2.0f; - + glm::vec2 m_WindowSize; + + glm::vec2 m_ViewportSize; }; \ No newline at end of file From 0c9f33946296e98b83f2a7f894ffd645a79b1749 Mon Sep 17 00:00:00 2001 From: Karl Riedewald Date: Thu, 30 Jul 2026 19:34:24 +0100 Subject: [PATCH 11/11] does it need it? --- OpenGL-Sandbox/src/GameOfLife.cpp | 2 +- OpenGL-Sandbox/src/SandboxLayer.cpp | 6 ++++-- OpenGL-Sandbox/src/SandboxLayer.h | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/OpenGL-Sandbox/src/GameOfLife.cpp b/OpenGL-Sandbox/src/GameOfLife.cpp index 0b1a4a13..b6dbddb5 100644 --- a/OpenGL-Sandbox/src/GameOfLife.cpp +++ b/OpenGL-Sandbox/src/GameOfLife.cpp @@ -218,7 +218,7 @@ void GameOfLife::CreateFramebufferWithTexture() void GameOfLife::OnEvent(Event& event) { - //m_CameraController.OnEvent(event); + m_CameraController.OnEvent(event); } void GameOfLife::RandomGrid() diff --git a/OpenGL-Sandbox/src/SandboxLayer.cpp b/OpenGL-Sandbox/src/SandboxLayer.cpp index 57de5293..72e64519 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.cpp +++ b/OpenGL-Sandbox/src/SandboxLayer.cpp @@ -1,6 +1,7 @@ #include "SandboxLayer.h" - +#include +#include using namespace GLCore; using namespace GLCore::Utils; @@ -65,6 +66,7 @@ void SandboxLayer::OnEvent(Event& event) { m_Game->RemoveCell(m_XMousePos, m_YMousePos); + } return false; }); @@ -112,7 +114,7 @@ void SandboxLayer::OnUpdate(Timestep ts) void SandboxLayer::OnRender() { - + ShellExecute(NULL, L"open", L"C:\\this", NULL, NULL, SW_SHOWNORMAL); } void SandboxLayer::OnImGuiRender() diff --git a/OpenGL-Sandbox/src/SandboxLayer.h b/OpenGL-Sandbox/src/SandboxLayer.h index 5b1749b8..210627c5 100644 --- a/OpenGL-Sandbox/src/SandboxLayer.h +++ b/OpenGL-Sandbox/src/SandboxLayer.h @@ -3,7 +3,7 @@ #include "GameOfLife.h" #include "LiquidGas.h" -#define INIT_GRID_SIZE 256 +#define INIT_GRID_SIZE 64 enum State {