Difference between revisions of "L4D Glow Effect"

From Valve Developer Community
Jump to: navigation, search
Line 3: Line 3:
 
== Introduction ==
 
== Introduction ==
 
This article will show you how to implement the Left For Dead (L4D) entity glow effect that is used for players, zombies, and items you can grab. This effect is not a shader and does not require you to edit any rendering code. I have included a couple of tweaking options and possibility for future enhancement.
 
This article will show you how to implement the Left For Dead (L4D) entity glow effect that is used for players, zombies, and items you can grab. This effect is not a shader and does not require you to edit any rendering code. I have included a couple of tweaking options and possibility for future enhancement.
 +
 +
This code relied heavily upon the masterful work of Jinto with his [http://www.facepunch.com/showpost.php?p=18536820&postcount=1078 LUA implementation] of this effect in Garry's Mod. Garry himself was also a big help in getting this into fruition and the hlcoders listserve.
  
 
== Requirements ==
 
== Requirements ==
 
*Strong C++ background
 
*Strong C++ background
 +
*Strong knowledge of the Source SDK
 
*Knowledge of general rendering process
 
*Knowledge of general rendering process
  
Line 122: Line 125:
  
 
The init function is called when the effect is created. This sets up all our materials and textures we will be using in order to render the effect. '''m_WhiteMaterial''' will be used to draw the model onto the stencil and the blur buffer in a constant shade of color. '''m_EffectMaterial''' is what the final result is going to be rendered from. '''m_GlowBuff1''' and '''m_GlowBuff2''' are our Render Targets for the glow effect and blurring. '''m_BlurX''' and '''m_BlurY''' are the blur shaders that will actually perform the blur.
 
The init function is called when the effect is created. This sets up all our materials and textures we will be using in order to render the effect. '''m_WhiteMaterial''' will be used to draw the model onto the stencil and the blur buffer in a constant shade of color. '''m_EffectMaterial''' is what the final result is going to be rendered from. '''m_GlowBuff1''' and '''m_GlowBuff2''' are our Render Targets for the glow effect and blurring. '''m_BlurX''' and '''m_BlurY''' are the blur shaders that will actually perform the blur.
 +
 +
=== CEntGlowEffect::Render ===
 +
This is the render function that is called every frame of the render process by the effect manager. This function builds the stencil with the entity cut-outs and the glow buffer with the entity images. It then blurs the glow buffer and renders the result to the screen while respecting the stencil.
 +
 +
<source lang=cpp>
 +
// Grab the render context
 +
CMatRenderContextPtr pRenderContext( materials );
 +
 +
// Apply our glow buffers as the base textures for our blurring operators
 +
IMaterialVar *var;
 +
// Set our effect material to have a base texture of our primary glow buffer
 +
var = m_BlurX->FindVar( "$basetexture", NULL );
 +
var->SetTextureValue( m_GlowBuff1 );
 +
var = m_BlurY->FindVar( "$basetexture", NULL );
 +
var->SetTextureValue( m_GlowBuff2 );
 +
var = m_EffectMaterial->FindVar( "$basetexture", NULL );
 +
var->SetTextureValue( m_GlowBuff1 );
 +
 +
var = m_BlurX->FindVar( "$bloomscale", NULL );
 +
var->SetFloatValue( 10*cl_ge_glowscale.GetFloat() );
 +
var = m_BlurY->FindVar( "$bloomamount", NULL );
 +
var->SetFloatValue( 10*cl_ge_glowscale.GetFloat() );
 +
 +
// Clear the glow buffer from the previous iteration
 +
pRenderContext->ClearColor4ub( 0, 0, 0, 255 );
 +
pRenderContext->PushRenderTargetAndViewport( m_GlowBuff1 );
 +
pRenderContext->ClearBuffers( true, true );
 +
pRenderContext->PopRenderTargetAndViewport();
 +
 +
pRenderContext->PushRenderTargetAndViewport( m_GlowBuff2 );
 +
pRenderContext->ClearBuffers( true, true );
 +
pRenderContext->PopRenderTargetAndViewport();
 +
 +
// Clear the stencil buffer in case someone dirtied it this frame
 +
pRenderContext->ClearStencilBufferRectangle( 0, 0, ScreenWidth(), ScreenHeight(), 0 );
 +
</source>
 +
 +
This part takes the materials that we initiated above and assigns their base textures to be the glow buffers respectively. It also manages the global scaling of the blur effect. From there it clears the glow buffers by setting them to a uniform Black color. We set it to black since '''m_EffectMaterial''' is set to be additive and black will not apply any effect to the screen (desired).
 +
 +
<source lang=cpp>
 +
// Iterate over our registered entities and add them to the cut-out stencil and the glow buffer
 +
for ( int i=0; i < m_vGlowEnts.Count(); i++ )
 +
{
 +
if ( cl_ge_glowstencil.GetInt() )
 +
RenderToStencil( i, pRenderContext );
 +
RenderToGlowTexture( i, pRenderContext );
 +
}
 +
</source>
 +
 +
This code iterates over all our registered entities and puts them into our stencil and the glow buffer.
 +
 +
<source lang=cpp>
 +
// Now we take the built up glow buffer (m_GlowBuff1) and blur it two ways
 +
// the intermediate buffer (m_GlowBuff2) allows us to do this properly
 +
pRenderContext->PushRenderTargetAndViewport( m_GlowBuff2 );
 +
pRenderContext->DrawScreenSpaceQuad( m_BlurX );
 +
pRenderContext->PopRenderTargetAndViewport();
 +
 +
pRenderContext->PushRenderTargetAndViewport( m_GlowBuff1 );
 +
pRenderContext->DrawScreenSpaceQuad( m_BlurY );
 +
pRenderContext->PopRenderTargetAndViewport();
 +
 +
if ( cl_ge_glowstencil.GetInt() )
 +
{
 +
// Setup the renderer to only draw where the stencil is not 1
 +
pRenderContext->SetStencilEnable( true );
 +
pRenderContext->SetStencilReferenceValue( 0 );
 +
pRenderContext->SetStencilTestMask( 1 );
 +
pRenderContext->SetStencilCompareFunction( STENCILCOMPARISONFUNCTION_EQUAL );
 +
pRenderContext->SetStencilPassOperation( STENCILOPERATION_ZERO );
 +
}
 +
 +
// Finally draw our blurred result onto the screen
 +
pRenderContext->DrawScreenSpaceQuad( m_EffectMaterial );
 +
 +
pRenderContext->SetStencilEnable( false );
 +
</source>
 +
 +
Finally, we take our built up buffers and blur them. This is done in a two step process (industry standard) by first blurring '''m_GlowBuff1''' in the X (Horizontal) direction and rendering it to '''m_GlowBuff2'''. Then '''m_GlowBuff2''' is blurred in the Y (vertical) direction and rendered back onto '''m_GlowBuff1'''. The stencil buffer is then called upon again to ensure that the rendering is only performed where the entity is not.
 +
 +
=== CEntGlowEffect::RenderToStencil ===
 +
This function renders the entity to the stencil. I will take this in small chunks so you understand more about the stencil buffer.
 +
 +
<source lang=cpp>
 +
pRenderContext->SetStencilEnable( true );
 +
pRenderContext->SetStencilFailOperation( STENCILOPERATION_KEEP );
 +
pRenderContext->SetStencilZFailOperation( STENCILOPERATION_KEEP );
 +
pRenderContext->SetStencilPassOperation( STENCILOPERATION_REPLACE );
 +
</source>
 +
 +
Here we enable the stencil, which automatically puts it as our Render Target. ''Modifying the render target while the stencil is active has disastrous results!'' We setup the stencil mask operations by saying that if we fail to match our reference value we keep the reference value, but if we pass the reference we replace it with the write mask value.
 +
 +
<source lang=cpp>
 +
pRenderContext->SetStencilCompareFunction( STENCILCOMPARISONFUNCTION_ALWAYS );
 +
pRenderContext->SetStencilWriteMask( 1 );
 +
pRenderContext->SetStencilReferenceValue( 1 );
 +
</source>
 +
 +
The compare function checks to see if we should even evaluate the pass/fail operation. The Write Mask is what will be written to the stencil if we pass.
 +
 +
<source lang=cpp>
 +
pRenderContext->DepthRange( 0.0f, 0.01f );
 +
render->SetBlend( 0 );
 +
 +
modelrender->ForcedMaterialOverride( m_WhiteMaterial );
 +
pEnt->DrawModel( STUDIO_RENDER );
 +
modelrender->ForcedMaterialOverride( NULL );
 +
 +
render->SetBlend( 1 );
 +
pRenderContext->DepthRange( 0.0f, 1.0f );
 +
</source>
 +
 +
Here we do a couple of hacky things to make sure we write appropriately to the stencil buffer. We set the depth range to basically nil so that no shading occurs. We also set the blending to 0 to make sure we don't impart any color information. We draw the model overriding it's material to '''m_WhiteMaterial''' and then reset everything back to it's nominal values.
 +
 
[[Category:Programming]]
 
[[Category:Programming]]

Revision as of 20:23, 27 November 2009

Introduction

This article will show you how to implement the Left For Dead (L4D) entity glow effect that is used for players, zombies, and items you can grab. This effect is not a shader and does not require you to edit any rendering code. I have included a couple of tweaking options and possibility for future enhancement.

This code relied heavily upon the masterful work of Jinto with his LUA implementation of this effect in Garry's Mod. Garry himself was also a big help in getting this into fruition and the hlcoders listserve.

Requirements

  • Strong C++ background
  • Strong knowledge of the Source SDK
  • Knowledge of general rendering process

The Breakdown

Rendering the glow effect requires three distinct steps. The first step is to draw the entity onto the engine's Stencil Buffer. The second step is to draw the entity with the color of the glow you want onto a render buffer. The third step is to blur the render buffer and draw it onto the screen while respecting the Stencil Buffer.

Buffers

Stencil Buffer: A grayscale buffer implemented by DirectX that can be written to and read from much like a mask in Photoshop. The Stencil Buffer is used in the Scratch Source SDK to read the ambient occlusion depth map and apply appropriate shading in areas. We will be using the Stencil Buffer to "cutout" the entities we want to glow so only the blurred image that extends beyond the entity will show giving the effect of a halo.

Render Target (Buffer): A Render Target is a special texture that is registered with the engine and can be written to by the renderer. We will declare two render targets (buffers). One will hold the cumulative images from the glowing entities and the other will be used to blur with.

Material vs. Texture

When dealing with the rendering engine it is important to understand the distinction between a material and a texture.

Material: A material encapsulates a particular shader and it's associated parameters. A material is what defines how the renderer should render.

Texture: A texture is pure image information. To set the texture of a material you typically use $basetexture and whenever the material is rendered that texture is used in the shader

Different Rendering Components

There are three distinct rendering components in the source engine.

  • IMatRenderContext (pRenderContext)
  • IVRenderView (render)
  • IVModelRender (modelrender)

We will be using all three of these in order to achieve the desired effect. Each one does its part to draw our world, however the details are beyond the scope of this tutorial.

The Code

I am going to present the code in chunks to discuss what each does. You can download the code in it's entirety here:

ge_screeneffects.cpp
ge_screeneffects.h

Header

class CEntGlowEffect : public IScreenSpaceEffect
{
public:
	CEntGlowEffect( void ) { };
 
	virtual void Init( void );
	virtual void Shutdown( void );
	virtual void SetParameters( KeyValues *params ) {};
	virtual void Enable( bool bEnable ) {};
	virtual bool IsEnabled( void ) { return true; }
 
	virtual void RegisterEnt( EHANDLE hEnt, Color glowColor = Color(255,255,255,64), float fGlowScale = 1.0f );
	virtual void DeregisterEnt( EHANDLE hEnt );
 
	virtual void SetEntColor( EHANDLE hEnt, Color glowColor );
	virtual void SetEntGlowScale( EHANDLE hEnt, float fGlowScale );
 
	virtual void Render( int x, int y, int w, int h );
 
protected:
	int FindGlowEnt( EHANDLE hEnt );
	void RenderToStencil( int idx, IMatRenderContext *pRenderContext );
	void RenderToGlowTexture( int idx, IMatRenderContext *pRenderContext );
 
private:
	struct sGlowEnt
	{
		EHANDLE	m_hEnt;
		float	m_fColor[4];
		float	m_fGlowScale;
	};
 
	CUtlVector<sGlowEnt*>	m_vGlowEnts;
 
	CTextureReference	m_GlowBuff1;
	CTextureReference	m_GlowBuff2;
 
	CMaterialReference	m_WhiteMaterial;
	CMaterialReference	m_EffectMaterial;
 
	CMaterialReference	m_BlurX;
	CMaterialReference	m_BlurY;
};

The glow effect inherits from IScreenSpaceEffect which is part of the screen space effect manager that processes effects during the render sequence. The effects are processed after the Viewmodel is rendered, color correction is applied, and HDR is applied, but before the HUD is drawn. This means the glow effect will be unaffected by HDR and color correction (GOOD!) but will show through the viewmodel (Maybe BAD?). Unfortunately, there is no real good way to solve this problem without adversely affecting the rendering of the effect or the viewmodel.

Anyway, all the functions are implemented from the abstract class, but I added a couple (ie. RegisterEnt and DeregisterEnt) which allow us to easily add and remove entities to apply the glow effect on.

CEntGlowEffect::Init()

ADD_SCREENSPACE_EFFECT( CEntGlowEffect, ge_entglow );

void CEntGlowEffect::Init( void ) 
{
	// Initialize the white overlay material to render our model with
	KeyValues *pVMTKeyValues = new KeyValues( "VertexLitGeneric" );
	pVMTKeyValues->SetString( "$basetexture", "vgui/white" );
	pVMTKeyValues->SetInt( "$vertexcolor", 1 );
	pVMTKeyValues->SetInt( "$vertexalpha", 1 );
	pVMTKeyValues->SetInt( "$model", 1 );
	m_WhiteMaterial.Init( "__geglowwhite", TEXTURE_GROUP_CLIENT_EFFECTS, pVMTKeyValues );
 
	// Initialize the Effect material that will be blitted to the Frame Buffer
	pVMTKeyValues->Clear();
	pVMTKeyValues->SetName( "UnlitGeneric" );
	pVMTKeyValues->SetString( "$basetexture", "_rt_FullFrameFB" );
	pVMTKeyValues->SetInt( "$additive", 1 );
	m_EffectMaterial.Init( "__geglowcomposite", TEXTURE_GROUP_CLIENT_EFFECTS, pVMTKeyValues );
 
	// Initialize render targets for our blurring
	m_GlowBuff1.InitRenderTarget( ScreenWidth()/2, ScreenHeight()/2, RT_SIZE_DEFAULT, IMAGE_FORMAT_RGBA8888, MATERIAL_RT_DEPTH_SEPARATE, false, "_rt_geglowbuff1" );
	m_GlowBuff2.InitRenderTarget( ScreenWidth()/2, ScreenHeight()/2, RT_SIZE_DEFAULT, IMAGE_FORMAT_RGBA8888, MATERIAL_RT_DEPTH_SEPARATE, false, "_rt_geglowbuff2" );
 
	// Load the blur textures
	m_BlurX.Init( materials->FindMaterial("pp/ge_blurx", TEXTURE_GROUP_OTHER, true) );
	m_BlurY.Init( materials->FindMaterial("pp/ge_blury", TEXTURE_GROUP_OTHER, true) );
}

The first bit tells the effect manager about us and provides an interface to create the effect. More about that later in the Implementation stage.

The init function is called when the effect is created. This sets up all our materials and textures we will be using in order to render the effect. m_WhiteMaterial will be used to draw the model onto the stencil and the blur buffer in a constant shade of color. m_EffectMaterial is what the final result is going to be rendered from. m_GlowBuff1 and m_GlowBuff2 are our Render Targets for the glow effect and blurring. m_BlurX and m_BlurY are the blur shaders that will actually perform the blur.

CEntGlowEffect::Render

This is the render function that is called every frame of the render process by the effect manager. This function builds the stencil with the entity cut-outs and the glow buffer with the entity images. It then blurs the glow buffer and renders the result to the screen while respecting the stencil.

// Grab the render context
CMatRenderContextPtr pRenderContext( materials );
 
// Apply our glow buffers as the base textures for our blurring operators
IMaterialVar *var;
// Set our effect material to have a base texture of our primary glow buffer
var = m_BlurX->FindVar( "$basetexture", NULL );
var->SetTextureValue( m_GlowBuff1 );
var = m_BlurY->FindVar( "$basetexture", NULL );
var->SetTextureValue( m_GlowBuff2 );
var = m_EffectMaterial->FindVar( "$basetexture", NULL );
var->SetTextureValue( m_GlowBuff1 );
 
var = m_BlurX->FindVar( "$bloomscale", NULL );
var->SetFloatValue( 10*cl_ge_glowscale.GetFloat() );
var = m_BlurY->FindVar( "$bloomamount", NULL );
var->SetFloatValue( 10*cl_ge_glowscale.GetFloat() );
 
// Clear the glow buffer from the previous iteration
pRenderContext->ClearColor4ub( 0, 0, 0, 255 );
pRenderContext->PushRenderTargetAndViewport( m_GlowBuff1 );
pRenderContext->ClearBuffers( true, true );
pRenderContext->PopRenderTargetAndViewport();

pRenderContext->PushRenderTargetAndViewport( m_GlowBuff2 );
pRenderContext->ClearBuffers( true, true );
pRenderContext->PopRenderTargetAndViewport();
 
// Clear the stencil buffer in case someone dirtied it this frame
pRenderContext->ClearStencilBufferRectangle( 0, 0, ScreenWidth(), ScreenHeight(), 0 );

This part takes the materials that we initiated above and assigns their base textures to be the glow buffers respectively. It also manages the global scaling of the blur effect. From there it clears the glow buffers by setting them to a uniform Black color. We set it to black since m_EffectMaterial is set to be additive and black will not apply any effect to the screen (desired).

// Iterate over our registered entities and add them to the cut-out stencil and the glow buffer
for ( int i=0; i < m_vGlowEnts.Count(); i++ )
{
	if ( cl_ge_glowstencil.GetInt() )
		RenderToStencil( i, pRenderContext );
	RenderToGlowTexture( i, pRenderContext );
}

This code iterates over all our registered entities and puts them into our stencil and the glow buffer.

// Now we take the built up glow buffer (m_GlowBuff1) and blur it two ways
// the intermediate buffer (m_GlowBuff2) allows us to do this properly
pRenderContext->PushRenderTargetAndViewport( m_GlowBuff2 );
	pRenderContext->DrawScreenSpaceQuad( m_BlurX );
pRenderContext->PopRenderTargetAndViewport();
 
pRenderContext->PushRenderTargetAndViewport( m_GlowBuff1 );
	pRenderContext->DrawScreenSpaceQuad( m_BlurY );
pRenderContext->PopRenderTargetAndViewport();
 
if ( cl_ge_glowstencil.GetInt() )
{
	// Setup the renderer to only draw where the stencil is not 1
	pRenderContext->SetStencilEnable( true );
	pRenderContext->SetStencilReferenceValue( 0 );
	pRenderContext->SetStencilTestMask( 1 );
	pRenderContext->SetStencilCompareFunction( STENCILCOMPARISONFUNCTION_EQUAL );
	pRenderContext->SetStencilPassOperation( STENCILOPERATION_ZERO );
}
 
// Finally draw our blurred result onto the screen
pRenderContext->DrawScreenSpaceQuad( m_EffectMaterial );
 
pRenderContext->SetStencilEnable( false );

Finally, we take our built up buffers and blur them. This is done in a two step process (industry standard) by first blurring m_GlowBuff1 in the X (Horizontal) direction and rendering it to m_GlowBuff2. Then m_GlowBuff2 is blurred in the Y (vertical) direction and rendered back onto m_GlowBuff1. The stencil buffer is then called upon again to ensure that the rendering is only performed where the entity is not.

CEntGlowEffect::RenderToStencil

This function renders the entity to the stencil. I will take this in small chunks so you understand more about the stencil buffer.

pRenderContext->SetStencilEnable( true );
pRenderContext->SetStencilFailOperation( STENCILOPERATION_KEEP );
pRenderContext->SetStencilZFailOperation( STENCILOPERATION_KEEP );
pRenderContext->SetStencilPassOperation( STENCILOPERATION_REPLACE );

Here we enable the stencil, which automatically puts it as our Render Target. Modifying the render target while the stencil is active has disastrous results! We setup the stencil mask operations by saying that if we fail to match our reference value we keep the reference value, but if we pass the reference we replace it with the write mask value.

pRenderContext->SetStencilCompareFunction( STENCILCOMPARISONFUNCTION_ALWAYS );
pRenderContext->SetStencilWriteMask( 1 );
pRenderContext->SetStencilReferenceValue( 1 );

The compare function checks to see if we should even evaluate the pass/fail operation. The Write Mask is what will be written to the stencil if we pass.

pRenderContext->DepthRange( 0.0f, 0.01f );
render->SetBlend( 0 );
 
modelrender->ForcedMaterialOverride( m_WhiteMaterial );
	pEnt->DrawModel( STUDIO_RENDER );
modelrender->ForcedMaterialOverride( NULL );
 
render->SetBlend( 1 );
pRenderContext->DepthRange( 0.0f, 1.0f );

Here we do a couple of hacky things to make sure we write appropriately to the stencil buffer. We set the depth range to basically nil so that no shading occurs. We also set the blending to 0 to make sure we don't impart any color information. We draw the model overriding it's material to m_WhiteMaterial and then reset everything back to it's nominal values.