GL3: Implement underwater postprocess effect using framebuffer object

Can be disabled with gl3_usefbo 0.
Mostly this adds an underwater warping effect, like the soft-renderer,
and also like the vulkan renderer (the shader is based on the vk one).
When this is enabled, the v_blend effect (for fullscreen overlay with
one color, like when hit or to make the screen white-ish when under
water) is now applied in the shader used for rendering the FBO instead
of rendering a fullscreen quad in blendmode.
This commit is contained in:
Daniel Gibson 2022-04-25 17:11:53 +02:00
parent 4db6534ab3
commit 3d619a1441
5 changed files with 240 additions and 10 deletions

View file

@ -249,6 +249,27 @@ GL3_Draw_TileClear(int x, int y, int w, int h, char *pic)
drawTexturedRectangle(x, y, w, h, x/64.0f, y/64.0f, (x+w)/64.0f, (y+h)/64.0f);
}
void
GL3_DrawFrameBufferObject(int x, int y, int w, int h, GLuint fboTexture, const float v_blend[4])
{
qboolean underwater = (gl3_newrefdef.rdflags & RDF_UNDERWATER) != 0;
gl3ShaderInfo_t* shader = underwater ? &gl3state.si2DpostProcessWater
: &gl3state.si2DpostProcess;
GL3_UseProgram(shader->shaderProgram);
GL3_Bind(fboTexture);
if(underwater && shader->uniLmScalesOrTime != -1)
{
glUniform1f(shader->uniLmScalesOrTime, gl3_newrefdef.time);
}
if(shader->uniVblend != -1)
{
glUniform4fv(shader->uniVblend, 1, v_blend);
}
drawTexturedRectangle(x, y, w, h, 0, 1, 1, 0);
}
/*
* Fills a box of pixels with a single color
*/

View file

@ -126,6 +126,7 @@ cvar_t *gl_shadows;
cvar_t *gl3_debugcontext;
cvar_t *gl3_usebigvbo;
cvar_t *r_fixsurfsky;
cvar_t *gl3_usefbo;
// Yaw-Pitch-Roll
// equivalent to R_z * R_y * R_x where R_x is the trans matrix for rotating around X axis for aroundXdeg
@ -258,6 +259,8 @@ GL3_Register(void)
r_speeds = ri.Cvar_Get("r_speeds", "0", 0);
gl_finish = ri.Cvar_Get("gl_finish", "0", CVAR_ARCHIVE);
gl3_usefbo = ri.Cvar_Get("gl3_usefbo", "1", CVAR_ARCHIVE); // use framebuffer object for postprocess effects (water)
#if 0 // TODO!
//gl_lefthand = ri.Cvar_Get("hand", "0", CVAR_USERINFO | CVAR_ARCHIVE);
//gl_farsee = ri.Cvar_Get("gl_farsee", "0", CVAR_LATCH | CVAR_ARCHIVE);
@ -601,6 +604,11 @@ GL3_Init(void)
GL3_SurfInit();
glGenFramebuffers(1, &gl3state.ppFBO);
// the rest for the FBO is done dynamically in GL3_RenderView() so it can
// take the viewsize into account (enforce that by setting invalid size)
gl3state.ppFBtexWidth = gl3state.ppFBtexHeight = -1;
R_Printf(PRINT_ALL, "\n");
return true;
}
@ -623,6 +631,17 @@ GL3_Shutdown(void)
GL3_SurfShutdown();
GL3_Draw_ShutdownLocal();
GL3_ShutdownShaders();
// free the postprocessing FBO and its renderbuffer and texture
if(gl3state.ppFBrbo != 0)
glDeleteRenderbuffers(1, &gl3state.ppFBrbo);
if(gl3state.ppFBtex != 0)
glDeleteTextures(1, &gl3state.ppFBtex);
if(gl3state.ppFBO != 0)
glDeleteFramebuffers(1, &gl3state.ppFBO);
gl3state.ppFBrbo = gl3state.ppFBtex = gl3state.ppFBO = 0;
gl3state.ppFBObound = false;
gl3state.ppFBtexWidth = gl3state.ppFBtexHeight = -1;
}
/* shutdown OS specific OpenGL stuff like contexts, etc. */
@ -1302,6 +1321,8 @@ GL3_MYgluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zF
return ret;
}
static void GL3_Clear(void);
static void
SetupGL(void)
{
@ -1332,7 +1353,67 @@ SetupGL(void)
}
#endif // 0
glViewport(x, y2, w, h);
// set up the FBO accordingly, but only if actually rendering the world
// (=> don't use FBO when rendering the playermodel in the player menu)
if (gl3_usefbo->value && (gl3_newrefdef.rdflags & RDF_NOWORLDMODEL) == 0 && gl3state.ppFBO != 0)
{
glBindFramebuffer(GL_FRAMEBUFFER, gl3state.ppFBO);
gl3state.ppFBObound = true;
if(gl3state.ppFBtex == 0)
{
gl3state.ppFBtexWidth = -1; // make sure we generate the texture storage below
glGenTextures(1, &gl3state.ppFBtex);
}
if(gl3state.ppFBrbo == 0)
{
gl3state.ppFBtexWidth = -1; // make sure we generate the RBO storage below
glGenRenderbuffers(1, &gl3state.ppFBrbo);
}
// even if the FBO already has a texture and RBO, the viewport size
// might have changed so they need to be regenerated with the correct sizes
if(gl3state.ppFBtexWidth != w || gl3state.ppFBtexHeight != h)
{
gl3state.ppFBtexWidth = w;
gl3state.ppFBtexHeight = h;
GL3_Bind(gl3state.ppFBtex);
// create texture for FBO with size of the viewport
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 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);
GL3_Bind(0);
// attach it to currently bound FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gl3state.ppFBtex, 0);
// also create a renderbuffer object so the FBO has a stencil- and depth-buffer
glBindRenderbuffer(GL_RENDERBUFFER, gl3state.ppFBrbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// attach it to the FBO
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
GL_RENDERBUFFER, gl3state.ppFBrbo);
GLenum fbState = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(fbState != GL_FRAMEBUFFER_COMPLETE)
{
R_Printf(PRINT_ALL, "GL3 SetupGL(): WARNING: FBO is not complete, status = 0x%x\n", fbState);
gl3state.ppFBtexWidth = -1; // to try again next frame; TODO: maybe give up?
gl3state.ppFBObound = false;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
GL3_Clear(); // clear the FBO that's bound now
glViewport(0, 0, w, h); // this will be moved to the center later, so no x/y offset
}
else // rendering directly (not to FBO for postprocessing)
{
glViewport(x, y2, w, h);
}
/* set up projection matrix (eye coordinates -> clip coordinates) */
{
@ -1637,13 +1718,23 @@ GL3_RenderFrame(refdef_t *fd)
{
GL3_RenderView(fd);
GL3_SetLightLevel(NULL);
qboolean usedFBO = gl3state.ppFBObound; // if it was/is used this frame
if(usedFBO)
{
glBindFramebuffer(GL_FRAMEBUFFER, 0); // now render to default framebuffer
gl3state.ppFBObound = false;
}
GL3_SetGL2D();
if(v_blend[3] != 0.0f)
int x = (vid.width - gl3_newrefdef.width)/2;
int y = (vid.height - gl3_newrefdef.height)/2;
if (usedFBO)
{
// if we're actually drawing the world and using an FBO, render the FBO's texture
GL3_DrawFrameBufferObject(x, y, gl3_newrefdef.width, gl3_newrefdef.height, gl3state.ppFBtex, v_blend);
}
else if(v_blend[3] != 0.0f)
{
int x = (vid.width - gl3_newrefdef.width)/2;
int y = (vid.height - gl3_newrefdef.height)/2;
GL3_Draw_Flash(v_blend, x, y, gl3_newrefdef.width, gl3_newrefdef.height);
}
}

View file

@ -217,6 +217,87 @@ static const char* fragmentSrc2D = MULTILINE_STRING(#version 150\n
}
);
static const char* fragmentSrc2Dpostprocess = MULTILINE_STRING(#version 150\n
in vec2 passTexCoord;
// for UBO shared between all shaders (incl. 2D)
// TODO: not needed here, remove?
layout (std140) uniform uniCommon
{
float gamma;
float intensity;
float intensity2D; // for HUD, menu etc
vec4 color;
};
uniform sampler2D tex;
uniform vec4 v_blend;
out vec4 outColor;
void main()
{
// no gamma or intensity here, it has been applied before
// (this is just for postprocessing)
vec4 res = texture(tex, passTexCoord);
// apply the v_blend, usually blended as a colored quad with:
// glBlendEquation(GL_FUNC_ADD); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
res.rgb = v_blend.a * v_blend.rgb + (1.0 - v_blend.a)*res.rgb;
outColor = res;
}
);
static const char* fragmentSrc2DpostprocessWater = MULTILINE_STRING(#version 150\n
in vec2 passTexCoord;
// for UBO shared between all shaders (incl. 2D)
// TODO: not needed here, remove?
layout (std140) uniform uniCommon
{
float gamma;
float intensity;
float intensity2D; // for HUD, menu etc
vec4 color;
};
const float PI = 3.14159265358979323846;
uniform sampler2D tex;
uniform float time;
uniform vec4 v_blend;
out vec4 outColor;
void main()
{
vec2 uv = passTexCoord;
// warping based on vkquake2
// here uv is always between 0 and 1 so ignore all that scrWidth and gl_FragCoord stuff
//float sx = pc.scale - abs(pc.scrWidth / 2.0 - gl_FragCoord.x) * 2.0 / pc.scrWidth;
//float sy = pc.scale - abs(pc.scrHeight / 2.0 - gl_FragCoord.y) * 2.0 / pc.scrHeight;
float sx = 1.0 - abs(0.5-uv.x)*2.0;
float sy = 1.0 - abs(0.5-uv.y)*2.0;
float xShift = 2.0 * time + uv.y * PI * 10;
float yShift = 2.0 * time + uv.x * PI * 10;
vec2 distortion = vec2(sin(xShift) * sx, sin(yShift) * sy) * 0.00666;
uv += distortion;
uv = clamp(uv, vec2(0.0, 0.0), vec2(1.0, 1.0));
// no gamma or intensity here, it has been applied before
// (this is just for postprocessing)
vec4 res = texture(tex, uv);
// apply the v_blend, usually blended as a colored quad with:
// glBlendEquation(GL_FUNC_ADD); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
res.rgb = v_blend.a * v_blend.rgb + (1.0 - v_blend.a)*res.rgb;
outColor = res;
}
);
// 2D color only rendering, GL3_Draw_Fill(), GL3_Draw_FadeScreen()
static const char* vertexSrc2Dcolor = MULTILINE_STRING(#version 150\n
@ -827,7 +908,8 @@ initShader2D(gl3ShaderInfo_t* shaderInfo, const char* vertSrc, const char* fragS
//shaderInfo->uniColor = shaderInfo->uniProjMatrix = shaderInfo->uniModelViewMatrix = -1;
shaderInfo->shaderProgram = 0;
shaderInfo->uniLmScales = -1;
shaderInfo->uniLmScalesOrTime = -1;
shaderInfo->uniVblend = -1;
shaders2D[0] = CompileShader(GL_VERTEX_SHADER, vertSrc, NULL);
if(shaders2D[0] == 0) return false;
@ -894,6 +976,18 @@ initShader2D(gl3ShaderInfo_t* shaderInfo, const char* vertSrc, const char* fragS
goto err_cleanup;
}
shaderInfo->uniLmScalesOrTime = glGetUniformLocation(prog, "time");
if(shaderInfo->uniLmScalesOrTime != -1)
{
glUniform1f(shaderInfo->uniLmScalesOrTime, 0.0f);
}
shaderInfo->uniVblend = glGetUniformLocation(prog, "v_blend");
if(shaderInfo->uniVblend != -1)
{
glUniform4f(shaderInfo->uniVblend, 0, 0, 0, 0);
}
return true;
err_cleanup:
@ -917,7 +1011,8 @@ initShader3D(gl3ShaderInfo_t* shaderInfo, const char* vertSrc, const char* fragS
}
shaderInfo->shaderProgram = 0;
shaderInfo->uniLmScales = -1;
shaderInfo->uniLmScalesOrTime = -1;
shaderInfo->uniVblend = -1;
shaders3D[0] = CompileShader(GL_VERTEX_SHADER, vertexCommon3D, vertSrc);
if(shaders3D[0] == 0) return false;
@ -1017,7 +1112,7 @@ initShader3D(gl3ShaderInfo_t* shaderInfo, const char* vertSrc, const char* fragS
}
GLint lmScalesLoc = glGetUniformLocation(prog, "lmScales");
shaderInfo->uniLmScales = lmScalesLoc;
shaderInfo->uniLmScalesOrTime = lmScalesLoc;
if(lmScalesLoc != -1)
{
shaderInfo->lmScales[0] = HMM_Vec4(1.0f, 1.0f, 1.0f, 1.0f);
@ -1102,6 +1197,17 @@ static qboolean createShaders(void)
return false;
}
if(!initShader2D(&gl3state.si2DpostProcess, vertexSrc2D, fragmentSrc2Dpostprocess))
{
R_Printf(PRINT_ALL, "WARNING: Failed to create shader program to render framebuffer object!\n");
return false;
}
if(!initShader2D(&gl3state.si2DpostProcessWater, vertexSrc2D, fragmentSrc2DpostprocessWater))
{
R_Printf(PRINT_ALL, "WARNING: Failed to create shader program to render framebuffer object under water!\n");
return false;
}
const char* lightmappedFrag = (gl3_colorlight->value == 0.0f)
? fragmentSrc3DlmNoColor : fragmentSrc3Dlm;

View file

@ -332,7 +332,7 @@ UpdateLMscales(const hmm_vec4 lmScales[MAX_LIGHTMAPS_PER_SURFACE], gl3ShaderInfo
if(hasChanged)
{
glUniform4fv(si->uniLmScales, MAX_LIGHTMAPS_PER_SURFACE, si->lmScales[0].Elements);
glUniform4fv(si->uniLmScalesOrTime, MAX_LIGHTMAPS_PER_SURFACE, si->lmScales[0].Elements);
}
}

View file

@ -116,7 +116,8 @@ typedef struct
typedef struct
{
GLuint shaderProgram;
GLint uniLmScales;
GLint uniVblend;
GLint uniLmScalesOrTime; // for 3D it's lmScales, for 2D underwater PP it's time
hmm_vec4 lmScales[4];
} gl3ShaderInfo_t;
@ -196,6 +197,13 @@ typedef struct
int currentlightmap; // lightmap_textureIDs[currentlightmap] bound to GL_TEXTURE1
GLuint currenttmu; // GL_TEXTURE0 or GL_TEXTURE1
// FBO for postprocess effects (like under-water-warping)
GLuint ppFBO;
GLuint ppFBtex; // ppFBO's texture for color buffer
int ppFBtexWidth, ppFBtexHeight;
GLuint ppFBrbo; // ppFBO's renderbuffer object for depth and stencil buffer
qboolean ppFBObound; // is it currently bound (rendered to)?
//float camera_separation;
//enum stereo_modes stereo_mode;
@ -208,6 +216,9 @@ typedef struct
// NOTE: make sure si2D is always the first shaderInfo (or adapt GL3_ShutdownShaders())
gl3ShaderInfo_t si2D; // shader for rendering 2D with textures
gl3ShaderInfo_t si2Dcolor; // shader for rendering 2D with flat colors
gl3ShaderInfo_t si2DpostProcess; // shader to render postprocess FBO, when *not* underwater
gl3ShaderInfo_t si2DpostProcessWater; // shader to apply water-warp postprocess effect
gl3ShaderInfo_t si3Dlm; // a regular opaque face (e.g. from brush) with lightmap
// TODO: lm-only variants for gl_lightmap 1
gl3ShaderInfo_t si3Dtrans; // transparent is always w/o lightmap
@ -393,6 +404,7 @@ extern void GL3_Draw_PicScaled(int x, int y, char *pic, float factor);
extern void GL3_Draw_StretchPic(int x, int y, int w, int h, char *pic);
extern void GL3_Draw_CharScaled(int x, int y, int num, float scale);
extern void GL3_Draw_TileClear(int x, int y, int w, int h, char *pic);
extern void GL3_DrawFrameBufferObject(int x, int y, int w, int h, GLuint fboTexture, const float v_blend[4]);
extern void GL3_Draw_Fill(int x, int y, int w, int h, int c);
extern void GL3_Draw_FadeScreen(void);
extern void GL3_Draw_Flash(const float color[4], float x, float y, float w, float h);