Monday, November 24, 2008

Render-to-Texture and 2D Lookup Refraction

This post will be a discussion on rendering to texture in OpenGL and using that texture in a shader to perform a pseudo-refraction on a 3D mesh in the foreground of the scene.

Rendering to texture can be done using OpenGL's framebuffer extension. I find it easiest to write an object that wraps up this functionality in a singleton manager which can be used anywhere in the code. The constructor creates the framebuffer object (FBO) as well as any other render objects required (e.g., for storing depth). The destructor cleans up these objects and the rest of the singleton provides functions for initializing a render-to-texture, binding the FBO, unbinding the FBO and checking for errors. I've posted the code for the FBOManager.cpp file below:

FBOManager* FBOManager::instance = NULL;

FBOManager::FBOManager() : fboID(0), renderBuffID(0) {
// Init the framebuffer and renderbuffer ID for binding,
// but don't bind the FBO just yet
glGenFramebuffersEXT(1, &this->fboID);
glGenRenderbuffersEXT(1, &this->renderBuffID);
}

FBOManager::~FBOManager() {
glDeleteFramebuffersEXT(1, &this->fboID);
glDeleteRenderbuffersEXT(1, &this->renderBuffID);
}

/**
* Setup the Framebuffer object to render to a specific
* texture type with a specific texture ID
* at a given height and width - call this BEFORE binding.
* Returns: true on success, false otherwise.
*/
bool FBOManager::SetupFBO(Texture& texture) {
texture.BindTexture();

glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, this->fboID);
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT,
this->renderBuffID);

glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT,
GL_DEPTH_COMPONENT,texture.GetWidth(), texture.GetHeight());
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT,
GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT,
this->renderBuffID);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
GL_COLOR_ATTACHMENT0_EXT, texture.GetTextureType(),
texture.GetTextureID(), 0);

GLenum buffers[] = { GL_COLOR_ATTACHMENT0_EXT };
glDrawBuffers(1, buffers);

glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

texture.UnbindTexture();

// Make sure everything FBO is good to go
return this->CheckFBOStatus();
}

/**
* Private helper function that checks the status of the FBO
* and reports any errors.
* Returns: true on successful status, false if badness.
*/
bool FBOManager::CheckFBOStatus() {
int status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
if (status != GL_FRAMEBUFFER_COMPLETE_EXT) {
switch (status) {
case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
debug_output("Framebuffer Object error detected:
Incomplete attachment.");
break;
case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
debug_output("Framebuffer Object error detected:
Incomplete dimensions.");
break;
case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
debug_output("Framebuffer Object error detected:
Incomplete draw buffer.");
break;
case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
debug_output("Framebuffer Object error detected:
Incomplete formats.");
break;
case GL_FRAMEBUFFER_INCOMPLETE_LAYER_COUNT_EXT:
debug_output("Framebuffer Object error detected:
Incomplete layer count.");
break;
case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS_EXT:
debug_output("Framebuffer Object error detected:
Incomplete layer targets.");
break;
case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
debug_output("Framebuffer Object error detected:
Incomplete, missing attachment.");
break;
case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE_EXT:
debug_output("Framebuffer Object error detected:
Incomplete multisample.");
break;
case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
debug_output("Framebuffer Object error detected:
Incomplete read buffer.");
break;
case GL_FRAMEBUFFER_UNSUPPORTED_EXT:
debug_output("Framebuffer Object error detected:
Framebuffer unsupported.");
break;
default:
debug_output("Framebuffer Object error detected:
Unknown Error");
break;
}
return false;
}
return true;
}

And here's the code for using the FBOManager:

bool success = FBOManager::GetInstance()->SetupFBO(
*this->renderToTexBeforeBall);
assert(success);
FBOManager::GetInstance()->BindFBO();

// Draw the stuff you want rendered into a texture here ...

FBOManager::GetInstance()->UnbindFBO();
this->renderToTexBeforeBall->RenderTextureToFullscreenQuad();

For the game I've been building, I rendered just the background into the texture - this would provide later effects shaders with the ability to look-up into the background and do neat effects (I was specifically aiming for refraction). Here's a picture of what the render-to-texture looked like when placed on a full screen quad:



And here is the code used to render the quad with the scene texture:

void Texture2D::RenderTextureToFullscreenQuad() {
glPushAttrib(GL_VIEWPORT_BIT | GL_TEXTURE_BIT |
GL_LIGHTING_BIT | GL_LIST_BIT | GL_CURRENT_BIT |
GL_ENABLE_BIT | GL_TRANSFORM_BIT | GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT);

glPushAttrib(GL_TRANSFORM_BIT);
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
gluOrtho2D(viewport[0],viewport[2],viewport[1],viewport[3]);
glPopAttrib();

glDisable(GL_DEPTH_TEST);
glDisable(GL_LIGHTING);
glDisable(GL_BLEND);
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);
glPolygonMode(GL_FRONT, GL_FILL);

glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();

// Bind the texture and draw the full screen quad
this->BindTexture();

// Set the appropriate parameters for rendering
// the single fullscreen quad
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
Texture::SetFilteringParams(Texture::Nearest, GL_TEXTURE_2D);

glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glBegin(GL_QUADS);
glTexCoord2i(0, 0); glVertex2i(0, 0);
glTexCoord2i(1, 0); glVertex2i(this->width, 0);
glTexCoord2i(1, 1); glVertex2i(this->width, this->height);
glTexCoord2i(0, 1); glVertex2i(0, this->height);
glEnd();
this->UnbindTexture();

glPopMatrix();
glPopAttrib();

glPushAttrib(GL_TRANSFORM_BIT);
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glPopAttrib();
}

Using the background rendered to texture I could then produce a pseudo-refraction or displacement effect with the in-game ball (for when the user obtains the "invisi-ball" power-down item). I figured a good way to do this would be to use the normals of the ball to do an offset look-up into the scene texture within a pixel shader (I can also later modify this to use normal maps). I've posted the entire cgfx file below:

/**
* PostRefract.cgfx
* Author: Callum Hay
* The following is a post-processing effect that
* will take a given geometry and, using its normals,
* distort the given rendered scene quad in the area
* where that mesh would normally be rendered -
* the effect of this is a kind of 'cloaking/invisiblity'
* of the mesh.
*/

//// UN-TWEAKABLES - AUTOMATICALLY-TRACKED TRANSFORMS ///////
float4x4 WorldITXf;
float4x4 WvpXf;
float4x4 WorldXf;
float4x4 ViewIXf;

//// TWEAKABLE PARAMETERS ////////////
float WarpAmount;
float SceneWidth;
float SceneHeight;
float IndexOfRefraction;
float3 InvisiColour;

//////// TEXTURE /////////////////////
texture SceneTexture;
sampler2D SceneSampler = sampler_state {
Texture = ;
WrapS = ClampToEdge;
WrapT = ClampToEdge;
MinFilter = LinearMipMapLinear;
MagFilter = Linear;
};

//////// CONNECTOR DATA STRUCTURES ///////////

/* data from application vertex buffer */
struct appdata {
float3 Position : POSITION;
float4 UV : TEXCOORD0;
float4 Normal : NORMAL;
};

/* data passed from vertex shader to pixel shader */
struct vertexOutput {
float4 HPosition : POSITION;
float3 WorldNormal : TEXCOORD0;
float3 ProjPos : TEXCOORD1;
float3 WorldView : TEXCOORD2;
};

///////// VERTEX SHADING /////////////////////

vertexOutput PostRefract_VS(appdata IN) {
vertexOutput OUT = (vertexOutput)0;

float4 Po = float4(IN.Position.xyz,1);
float3 Pw = mul(WorldXf,Po).xyz;

float3 worldNormal = mul(WorldITXf,IN.Normal).xyz;
float3 viewToVert = Pw - float3(ViewIXf[0].w,ViewIXf[1].w,
ViewIXf[2].w);

OUT.WorldNormal = normalize(worldNormal);
OUT.WorldView = normalize(viewToVert);
float4 outPos = mul(WvpXf,Po);
OUT.HPosition = outPos;
OUT.ProjPos = outPos.xyz / outPos.w;
return OUT;
}

///////// PIXEL SHADING //////////////////////

float4 PostRefract_PS(vertexOutput IN) : COLOR {
float3 nWorldView = normalize(IN.WorldView.xyz);
float3 nWorldNormal = normalize(IN.WorldNormal.xyz);
float3 nProjPos = (IN.ProjPos.xyz + float3(1,1,1)) * 0.5;

// This is a hacked version of the Fresnel effect -
// it has fixed constants of 0.5 for the bias, 1
// for the Scale and 1 for the power
// The reasoning is that I want a nicely blended bit of
// both reflection and refraction especially in cases
// of total internal refraction (which looks ugly and
// abrupt without this)
float reflectionCoeff = max(0, min(1, 1.5f +
dot(nWorldView,nWorldNormal)));

// Find the world space reflection vector
float3 reflectionVec = reflect(nWorldView, nWorldNormal);
// Find the world space refraction vector
// (IndexOfRefraction_Medium to air)
float3 refractionVec = refract(nWorldView, nWorldNormal,
IndexOfRefraction/1.0003);

// Obtain the lookup vector which will
// be scaled/warped to get an offset for
// looking up the background/scene texture
float2 lookupVec = reflectionCoeff*reflectionVec.xy +
(1-reflectionCoeff)*refractionVec.xy;

// Use the refraction and scale it to the screen
// size to get an offset for looking up the texel
// in the scene texture
float2 lookupOffset = WarpAmount * lookupVec.xy *
float2(1.0/SceneWidth, 1.0/SceneHeight);
float3 textureColour = tex2D(SceneSampler, nProjPos.xy +
lookupOffset).rgb;

return float4(InvisiColour*textureColour, 1.0f);
}

///// TECHNIQUES /////////////////////////////
technique PostRefractGeom {
pass p0 < script = "Draw=geometry;"> {
BlendEnable = true;
DepthTestEnable = true;
DepthFunc = LEqual;
CullFaceEnable = true;
CullFace = Back;
PolygonMode = int2(Front, Fill);

VertexProgram = compile vp40 PostRefract_VS();
FragmentProgram = compile fp40 PostRefract_PS();
}
}

It's definitely noticeable when calculating the texture look-up in the pixel shader just how much I fudged the refraction - it's a very simple offset procedure with a very tweakable "WarpAmount" parameter, but with great results.
I find that an index of refraction of about 1.33 (water) and a warp amount around 50 gives a really nice "cloaking" effect (see the image below).