Archived

This topic is now archived and is closed to further replies.

OpenGL Wrapper for OpenGL, can't user member function WndProc

This topic is 5777 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Well I hope the entire Subject Header gets through on this one. I''m writing a wrapper for OpenGL (do I hear yawns at the back?) and I''ve come across one specific problem. When I try to register my window with the windows system I have to pass in the WndProc function for message handling. This function is in my OpenGL class and I can''t cast it to anything the Visual C++ SDK is interested in compiling. i.e. the error generally is error C2440: ''return'' : cannot convert from ''long (__stdcall COpenGL::*)(struct HWND__ *,unsigned int,unsigned int,long)'' to ''long (__stdcall *)(struct HWND__ *,unsigned int,unsigned int,long)'' i.e. it won''t accept a member function as the message handler. Is there a way around this or do I have to redesign? Cheers, Mike

Share this post


Link to post
Share on other sites
I am aware that I can get around this by making the WndProc function a static member, but that would involve abstracting the function to a higher level class that deals with an OpenGL window by passing the messages through, as my WndProc function currently touches member variables.

This raises one question I''m not quite sure about, is it possible to have multiple windowed OpenGL windows or does hardware acceleration just not work that way?

Forgive me, I''m an AI programmer and I know not what I do.

Mike

Share this post


Link to post
Share on other sites
MikeD is right, you can use a static member function for the callback. You then call a private member function on the correct window.

The trick then becomes figuring out which instance of the class belongs to this window. I've seen this done a number of ways...

One way is to use the SetWindowLong function to store a pointer to the object for each window. Then in your static member function, you get the HWND, and use GetWindowLong to retreive the pointer. Then you can call the correct private member like this...

pObject->PrivateWndProc().

The other cool way i've seen is to use an STL map or linked list to map HWNDs to object pointers. In your window creation function, store the HWND and this pointer in the map. Then you simply look up the HWND and retrieve the correct pointer.

Hope this works.

Chris Miller.

CosmicBee - Software With Buzz

[edited by - crizo on March 21, 2002 12:57:16 PM]

Share this post


Link to post
Share on other sites
My lead programmer at my last company (which fell 2 days ago) has an article in the pipeline for games programming gems 3 on an idea called autolists, where, using templates, a list is automatically stored with every instantiation of a class of the members of that class. So when you instantiate a window it would automatically have a list of the other windows as a static member. The static WndProc could access that list and, with each instantiation holding its own windows handle, you could use the static WndProc to traverse the list and find the suitable PrivateWndProc to call.

Either way, cheers crizo for a good direction to head in.

Mike

Share this post


Link to post
Share on other sites
I searched long and hard on how to use a non-static member function as my WindowProc as well, and I ended up going with what crizo was getting at.

Basically I pass the this pointer of the object that creates the window to my custom window creation function. In this function, right after CreateWindowEx I call SetProperty with the handle of the newly created window, a string for the property (e.g., "PrivateWindowProc") and the this pointer which was passed to the function.

My CWindow class has a static member function MainWindowProc (which is what I use in RegisterClass) which simply checks to see if any window that is sending it messages has a property called "PrivateWindowProc", and if so I do what crizo said:

return(pThis->PrivateWindowProc(hWnd,uMsg,wParam,lParam));

If the messaging window doesn''t have, or doesn''t yet have, a property for the this pointer then I just pass the message to DefWindowProc.

This way, the MainWindowProc passes all messages to my non-static member function, PrivateWindowProc, which can then access member data normally.

If you want to see exactly what I''m talking about let me know.

Care,
Chris Rasmus

Florida, USA
RTS Engine in Development
http://www.knology.net/~heaven
Jesus is LORD!

Share this post


Link to post
Share on other sites
You could have a static function that calls the WndProc function of the appropriate instance for you, using a static pointer to the current instance.

static OGLWrapperObj* OGLWrapperObj::currentObj;

static void OGLWrapperObj::CallFunc( void )
{
currentObj->WndProc();
}

Then you could simply make any instance the current instance...

void OGLWrapperObj::MakeCurrent( void )
{ currentObj = this; }

and call the function.



[edited by - smitty1276 on March 22, 2002 12:49:00 PM]

Share this post


Link to post
Share on other sites
Interesting, reading this thread has given me insight to pretty much the same problem I am experiencing.

I've been trying to create a "CWindowGL" class which would just create the actual Window and handle all the messages. However, I came across the WndProc problem. I've been trying to get my head around these replies, but it's difficult because I am quite new to Windows programming.

The one method of implementing a solution to the WndProc problem that intrests me is the one that Heaven posted. However, I am curious: What does the MainWndProc() function handle, and what does the PrivateWndProc() function handle? Can you maybe explain the relationship between these two in greater detail? Are you using MFC to make use of the SetProperty() func? Any help is appreciated, thanks!

PS:

Earlier today I was sort of able to get it working: I friended the WndProc function in my CWindowGL class, however my CPU usage jumped up to ~50% and nothing got drawn to the screen. I guess friending it isn't the optimal route... :D

[edited by - Wheaty on March 22, 2002 5:27:56 PM]

Share this post


Link to post
Share on other sites
The relationship between the static MainWndProc and the PrivateWndProc is as follows.
As you require a static (or non-member) function to pass into the WndProc pointer when you register the window, to handle messages, you have to have a MainWndProc to do the message handling. However you want each window to handle its own messages with its own member function that can access the member variables. So the MainWndProc must have access to all of the window instantiations so it can pass the message on to the correct window. So the MainWndProc handles passing the relavent message to the correct instantiation of the class of window.

If that makes sense.

Mike

Share this post


Link to post
Share on other sites
Ugh...

Well, I understand WHY this needs to be done, however I am confused over how to implement it. For example (not using GL stuff, just trying to get a regular window drawn -- I also chopped some member funcs out, just want to get this WndProc stuff straightend out):


        
class CWindowGL
{
protected:
HWND hWnd; // window handle

HDC hDC; // device context


private:
bool done; //on/off flag


public:
CWindowGL() { done = false; } // constructor


static LRESULT CALLBACK MainWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
LRESULT PrivateWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
};


[edit: whoops, moved the PrivateWndProc() to public: ]

So what you're trying to say is that the MainWndProc() would determine WHICH object to use, and then call that object's PrivateWndProc function. So there would be no need to use a switch(message) {... } in that function, because its only purpose would be to determine which PrivateWndProc to call. I understand that -- I guess where I'm stuck is how to store the pointer data. I saw Heaven referring to a "SetProperty" function... I checked my MSDN documentation and it seems to be a MFC call.

[edited by - Wheaty on March 22, 2002 6:46:04 PM]

Share this post


Link to post
Share on other sites
Hmm, I decided to use smitty1276''s idea and it worked. Just one question though, is it normal for regular Windows app which paints "Hello World!" to the middle of a form to utilize 100% of the CPU?

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
I can see how this solves the problem of wrapping a window in a class, but what about deallocating the object? If the WndProc is inside the class the WM_QUIT message can''t call "delete this". You could have a static STL map of HWND''s->this'' and deallocate objects in the static WndProc, but how do you notify the remaining portion of your application? The part that called the object''s constructor? You could send an event or post a message, but this seems to be getting out of hand. Wrapping a window in an object ought to be easier than this.

Share this post


Link to post
Share on other sites
Sort of makes me wonder if wrapping an OGL Window in a class is even worth the effort... :/

Share this post


Link to post
Share on other sites
quote:
Original post by Wheaty
The one method of implementing a solution to the WndProc problem that intrests me is the one that Heaven posted. However, I am curious: What does the MainWndProc() function handle, and what does the PrivateWndProc() function handle? Can you maybe explain the relationship between these two in greater detail? Are you using MFC to make use of the SetProperty() func? Any help is appreciated, thanks!


Let me show you how I''ve got things set up, and then how I use it (WindowProc). Keep in mind that I''ve stripped a lot of stuff not important in the relating of the concept.

class CWindow
{
BOOL Register(WNDPROC);
BOOL Create(CGameApp *);
HWND Handle;
}

BOOL CWindow::Register(WNDPROC WndProc)
{
WNDCLASS wc;
...
wc.lpfnWndProc=(WNDPROC)WndProc;
...
}

BOOL CWindow::Create(CGameApp *pThis)
{
...
if (!(Handle=CreateWindowEx(blah)))
return(FALSE);
SetProp(Handle,"WINDOWPROC",(HANDLE)pThis);
...
}

class CWinApp:public CWindow,
public CKeyboard,
public CMouse
{
BOOL Ready;
MSG Msg;
static LRESULT CALLBACK MainWindowProc(HWND,UINT,WPARAM,LPARAM);
BOOL Startup(CGameApp *);
}

BOOL CWinApp::Startup(CGameApp *pThis)
{
...
if (!Register(MainWindowProc))
return(FALSE);

if (!Create(pThis))
return(FALSE);
...
}

static LRESULT CALLBACK CWinApp::MainWindowProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
CGameApp *pThis=(CGameApp *)GetProp(hWnd,"WINDOWPROC");

if (pThis)
return(pThis->WindowProc(hWnd,msg,wParam,lParam));

return(DefWindowProc(hWnd,msg,wParam,lParam));
}

class CGameApp:public CWinApp,
public COpenGL, // could be CDirectX if I wanted to implement D3D style rendering
public CMap,
public CObjects
{
CGameApp(void);
LRESULT CALLBACK WindowProc(HWND,UINT,WPARAM,LPARAM);
}

CGameApp::CGameApp(void)
{
...
if (!Startup(this))
Ready=FALSE;
else
Ready=TRUE;
...
}

LRESULT CALLBACK CGameApp::WindowProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
switch(msg)
{
case WM_...:
{
...
return(0);
}
...
}
return(DefWindowProc(hWnd,msg,wParam,lParam));
}

int WINAPI WinMain(blah)
{
CGameApp GameApp;

if (!GameApp.Ready)
return(0);

// do main loop here
return(GameApp.Msg.wParam);
}

That should spell it out quite nicely for you.

If you have any questions please ask!

Care,
Chris Rasmus

Florida, USA
RTS Engine in Development
http://www.knology.net/~heaven
Jesus is LORD!

Share this post


Link to post
Share on other sites
It worked!!

Now I don''t need that global callback func anymore!

Btw, one prob I encountered with the methodes described above
is that the window "looses" a couple (4 last time i checked) of msg''s sent to the window when it''s created.
That ment that WM_CREATE never reached my WindowProc() =(

Ie.

CLASSNAME::Create()
{
CreateWindowEx(.....);
// I added a breakpoint in the MainWindowProc func, and it
// breaked before the one on SetProp.
SetProp(....);
}

Share this post


Link to post
Share on other sites
What you are trying to do is to write your own MFC.

Now if you could just use the search, you''d get LOADS of threads that answered this very question, and presented elegant solutions.

The way you do it is this.

When you register class, allocate 4 extra bytes per-window. Set cbWndExtra to 4.

When you call CreateWindow, it takes lpParam. Set it to "this". In your static WndProc, handle WM_NCCREATE. WM_NCCREATE''s lParam is an LPCREATESTRUCT which contains lpCreateParams. You set lpCreateParams earlier to "this". Now call SetWindowLong(hwnd, GWL_USERDATA, lpCreateParams). Don''t return yet.

Now for all messages, including WM_NCCREATE, do the following.

CWindow *pWindow = (CWindow *) GetWindowLong(hwnd, GWL_USERDATA);
pWindow->PrivateWndProc(hwnd, msg, wParam, lParam);

Your PrivateWndProc will get all messages from the first to the last. You should add a check to make sure pWindow is not NULL here.

Finally, if you allocated CWindow pointer with new, in your PrivateWndProc handle WM_NCDESTROY and do a delete this; in it.

It would be nice to add answers to WNDCLASS and link error questions to FAQ.

Share this post


Link to post
Share on other sites
IndirectX, here's what I came up with from your description:


LRESULT CALLBACK CWindow::MainWndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
switch (umsg)
{
case WM_NCCREATE:
SetWindowLong(hwnd, GWL_USERDATA, lparam);
default:
CWindow *pWindow = (CWindow*)GetWindowLong(hwnd, GWL_USERDATA);
if (pWindow)
pWindow->PrivWndProc(hwnd, umsg, wparam, lparam);
break;
}

return DefWindowProc(hwnd, umsg, wparam, lparam);
}

LRESULT CALLBACK CWindow:: PrivWndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
PAINTSTRUCT paintStruct;
char string[] = "Hello World!";

switch (umsg)
{
case WM_CREATE:
return 0;
break;

case WM_CLOSE:
PostQuitMessage(0);
return 0;
break;

case WM_PAINT:
hDC = BeginPaint(hwnd, &paintStruct);
SetTextColor(hDC, COLORREF(0x00FF0000));
TextOut(hDC, 150, 150, string, sizeof(string)-1);
EndPaint(hwnd, &paintStruct);
return 0;
break;

case WM_NCDESTROY:
delete this;
return 0;
break;

default:
return 0;
break;
}

return DefWindowProc(hwnd, umsg, wparam, lparam);
}

Everything seems to work fine, however, when I quit the program, it still remains running in memory.

[edited by - Wheaty on March 24, 2002 3:40:18 PM]

Share this post


Link to post
Share on other sites
if (pWindow) RETURN pWindow->PrivWndProc(hwnd, umsg, wparam, lparam);
return what you get from the windowprocedure..

dunno if this helps yet



[edited by - davepermen on March 24, 2002 4:30:48 PM]

Share this post


Link to post
Share on other sites
quote:
Original post by davepermen
if (pWindow) RETURN pWindow->PrivWndProc(hwnd, umsg, wparam, lparam);
return what you get from the windowprocedure..

dunno if this helps yet



[edited by - davepermen on March 24, 2002 4:30:48 PM]


Ah yes, actually it was a combination of returning the pWindow->PrivWndProc() call, and removing the "return 0;" from the "default:" action in PrivWndProc().

Here's the fixed code for anyone who's interested:


LRESULT CALLBACK CWindow:: PrivWndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
PAINTSTRUCT paintStruct;
char string[] = "Hello World!";

switch (umsg)
{
case WM_CREATE:
return 0;
break;

case WM_CLOSE:
PostQuitMessage(0);
return 0;
break;

case WM_PAINT:
hDC = BeginPaint(hwnd, &paintStruct);
SetTextColor(hDC, COLORREF(0x00FF0000));
TextOut(hDC, 150, 150, string, sizeof(string)-1);
EndPaint(hwnd, &paintStruct);
return 0;
break;

case WM_NCDESTROY:
delete this;
return 0;
break;

default:
break;
}

return DefWindowProc(hwnd, umsg, wparam, lparam);
}


LRESULT CALLBACK CWindow::MainWndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
switch (umsg)
{
case WM_NCCREATE:
SetWindowLong(hwnd, GWL_USERDATA, lparam);
default:
CWindow *pWindow = (CWindow*)GetWindowLong(hwnd, GWL_USERDATA);
if (pWindow)
return pWindow->PrivWndProc(hwnd, umsg, wparam, lparam);
break;
}

return DefWindowProc(hwnd, umsg, wparam, lparam);
}



[edited by - Wheaty on March 24, 2002 4:34:10 PM]

Share this post


Link to post
Share on other sites
I suggest you move PostQuitMessage to WM_NCDESTROY handler. You also don''t need to use break after return in switches.

Share this post


Link to post
Share on other sites
quote:
Original post by IndirectX
I suggest you move PostQuitMessage to WM_NCDESTROY handler. You also don''t need to use break after return in switches.

If I move the PostQuitMessage(0); call to WM_NCDESTROY, then it is impossible to close the window. Have to end it in the task manager.

Share this post


Link to post
Share on other sites
How did you get your code to work? "this" is the lpCreateParams of CREATESTRUCT. I meant the following:

SetWindowLong(hwnd, GWL_USERDATA, LPCREATESTRUCT(lparam)->lpCreateParams);

Am I missing something very obvious here?

Share this post


Link to post
Share on other sites
Well, it compiled with no errors.

If I put the line in that you just specified, I get this error:

error C2664: ''SetWindowLongA'' : cannot convert parameter 3 from ''LPVOID'' to ''LONG''

I suppose I have to cast a LONG to it or something.

Share this post


Link to post
Share on other sites
The proper way of closing the window is using DestroyWindow(hwnd) is WM_CLOSE and PostQuitMessage in WM_DESTROY, or WM_NCDESTROY.

If you click the close button or press Alt+F4, you get WM_CLOSE. However, to close a window from a program you call DestroyWindow(hwnd). Your method won''t work for DestroyWindow call.

Share this post


Link to post
Share on other sites

  • Similar Content

    • By xhcao
      Does sync be needed to read texture content after access texture image in compute shader?
      My simple code is as below,
      glUseProgram(program.get());
      glBindImageTexture(0, texture[0], 0, GL_FALSE, 3, GL_READ_ONLY, GL_R32UI);
      glBindImageTexture(1, texture[1], 0, GL_FALSE, 4, GL_WRITE_ONLY, GL_R32UI);
      glDispatchCompute(1, 1, 1);
      // Does sync be needed here?
      glUseProgram(0);
      glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
      glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                                     GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, texture[1], 0);
      glReadPixels(0, 0, kWidth, kHeight, GL_RED_INTEGER, GL_UNSIGNED_INT, outputValues);
       
      Compute shader is very simple, imageLoad content from texture[0], and imageStore content to texture[1]. Does need to sync after dispatchCompute?
    • By Jonathan2006
      My question: is it possible to transform multiple angular velocities so that they can be reinserted as one? My research is below:
      // This works quat quaternion1 = GEQuaternionFromAngleRadians(angleRadiansVector1); quat quaternion2 = GEMultiplyQuaternions(quaternion1, GEQuaternionFromAngleRadians(angleRadiansVector2)); quat quaternion3 = GEMultiplyQuaternions(quaternion2, GEQuaternionFromAngleRadians(angleRadiansVector3)); glMultMatrixf(GEMat4FromQuaternion(quaternion3).array); // The first two work fine but not the third. Why? quat quaternion1 = GEQuaternionFromAngleRadians(angleRadiansVector1); vec3 vector1 = GETransformQuaternionAndVector(quaternion1, angularVelocity1); quat quaternion2 = GEQuaternionFromAngleRadians(angleRadiansVector2); vec3 vector2 = GETransformQuaternionAndVector(quaternion2, angularVelocity2); // This doesn't work //quat quaternion3 = GEQuaternionFromAngleRadians(angleRadiansVector3); //vec3 vector3 = GETransformQuaternionAndVector(quaternion3, angularVelocity3); vec3 angleVelocity = GEAddVectors(vector1, vector2); // Does not work: vec3 angleVelocity = GEAddVectors(vector1, GEAddVectors(vector2, vector3)); static vec3 angleRadiansVector; vec3 angularAcceleration = GESetVector(0.0, 0.0, 0.0); // Sending it through one angular velocity later in my motion engine angleVelocity = GEAddVectors(angleVelocity, GEMultiplyVectorAndScalar(angularAcceleration, timeStep)); angleRadiansVector = GEAddVectors(angleRadiansVector, GEMultiplyVectorAndScalar(angleVelocity, timeStep)); glMultMatrixf(GEMat4FromEulerAngle(angleRadiansVector).array); Also how do I combine multiple angularAcceleration variables? Is there an easier way to transform the angular values?
    • By dpadam450
      I have this code below in both my vertex and fragment shader, however when I request glGetUniformLocation("Lights[0].diffuse") or "Lights[0].attenuation", it returns -1. It will only give me a valid uniform location if I actually use the diffuse/attenuation variables in the VERTEX shader. Because I use position in the vertex shader, it always returns a valid uniform location. I've read that I can share uniforms across both vertex and fragment, but I'm confused what this is even compiling to if this is the case.
       
      #define NUM_LIGHTS 2
      struct Light
      {
          vec3 position;
          vec3 diffuse;
          float attenuation;
      };
      uniform Light Lights[NUM_LIGHTS];
       
       
    • By pr033r
      Hello,
      I have a Bachelor project on topic "Implenet 3D Boid's algorithm in OpenGL". All OpenGL issues works fine for me, all rendering etc. But when I started implement the boid's algorithm it was getting worse and worse. I read article (http://natureofcode.com/book/chapter-6-autonomous-agents/) inspirate from another code (here: https://github.com/jyanar/Boids/tree/master/src) but it still doesn't work like in tutorials and videos. For example the main problem: when I apply Cohesion (one of three main laws of boids) it makes some "cycling knot". Second, when some flock touch to another it scary change the coordination or respawn in origin (x: 0, y:0. z:0). Just some streng things. 
      I followed many tutorials, change a try everything but it isn't so smooth, without lags like in another videos. I really need your help. 
      My code (optimalizing branch): https://github.com/pr033r/BachelorProject/tree/Optimalizing
      Exe file (if you want to look) and models folder (for those who will download the sources):
      http://leteckaposta.cz/367190436
      Thanks for any help...

    • By Andrija
      I am currently trying to implement shadow mapping into my project , but although i can render my depth map to the screen and it looks okay , when i sample it with shadowCoords there is no shadow.
      Here is my light space matrix calculation
      mat4x4 lightViewMatrix; vec3 sun_pos = {SUN_OFFSET * the_sun->direction[0], SUN_OFFSET * the_sun->direction[1], SUN_OFFSET * the_sun->direction[2]}; mat4x4_look_at(lightViewMatrix,sun_pos,player->pos,up); mat4x4_mul(lightSpaceMatrix,lightProjMatrix,lightViewMatrix); I will tweak the values for the size and frustum of the shadow map, but for now i just want to draw shadows around the player position
      the_sun->direction is a normalized vector so i multiply it by a constant to get the position.
      player->pos is the camera position in world space
      the light projection matrix is calculated like this:
      mat4x4_ortho(lightProjMatrix,-SHADOW_FAR,SHADOW_FAR,-SHADOW_FAR,SHADOW_FAR,NEAR,SHADOW_FAR); Shadow vertex shader:
      uniform mat4 light_space_matrix; void main() { gl_Position = light_space_matrix * transfMatrix * vec4(position, 1.0f); } Shadow fragment shader:
      out float fragDepth; void main() { fragDepth = gl_FragCoord.z; } I am using deferred rendering so i have all my world positions in the g_positions buffer
      My shadow calculation in the deferred fragment shader:
      float get_shadow_fac(vec4 light_space_pos) { vec3 shadow_coords = light_space_pos.xyz / light_space_pos.w; shadow_coords = shadow_coords * 0.5 + 0.5; float closest_depth = texture(shadow_map, shadow_coords.xy).r; float current_depth = shadow_coords.z; float shadow_fac = 1.0; if(closest_depth < current_depth) shadow_fac = 0.5; return shadow_fac; } I call the function like this:
      get_shadow_fac(light_space_matrix * vec4(position,1.0)); Where position is the value i got from sampling the g_position buffer
      Here is my depth texture (i know it will produce low quality shadows but i just want to get it working for now):
      sorry because of the compression , the black smudges are trees ... https://i.stack.imgur.com/T43aK.jpg
      EDIT: Depth texture attachment:
      glTexImage2D(GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT24,fbo->width,fbo->height,0,GL_DEPTH_COMPONENT,GL_FLOAT,NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, fbo->depthTexture, 0);
  • Popular Now