Dynamic RTT shadow angles in Source 2007

From Valve Developer Community
Revision as of 11:55, 21 November 2011 by Saul (talk | contribs) (Initial tutorial)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Introduction

This article explains how to implement dynamic render-to-texture shadow angles into your Source 2007 mod. Render-to-texture (now referred to as RTT) shadows are cast by most non-static renderables in the Source Engine. The direction of the shadow they cast is generally given by the shadow_control entity, and affects the shadow direction of all entities in a level. In Left 4 Dead, Portal 2 and Alien Swarm, the direction of a shadow is calculated on a per-entity basis, and dictated by the closest light to the entity. By copying available code from the Alien Swarm codebase into your Source 2007 mod, it is possible to implement this feature in your mod.

Replicating engine light functions

Unfortunately, the Alien Swarm code depends on a function (IVModelRender::GetBrightestShadowingLightSource) within the closed-source engine that is not available in Source 2007. Therefore, we will have to replicate its entire functionality (as well as some level loading code) in the game.

game/client/worldlight.cpp

Create a new file in your */game/client* folder called *worldlight.cpp*, and paste the following code into it. WARNING: by using the *worldlight.cpp* and or *worldlight.h* files in your mod, then I (Saul Rennison) must be attributed as a contributor within your mod credits.

game/client/worldlight.h

Now create *worldlight.h* within */game/client*, pasting in the following code: NOTE: don’t forget to add these files to your client project!

Updating the client shadow manager

We will now use the code available in Alien Swarm to enable dynamic shadow directions.

game/client/clientshadowmgr.cpp

Open *game/client/clientshadowmgr.cpp* and make the following changes: At approximately line 83, below:

#include "cmodel.h"

Add:

#include "debugoverlay_shared.h"
#include "worldlight.h"

At approximately line 94, below:

static ConVar r_flashlight_version2( "r_flashlight_version2", "0" );

Add:

void WorldLightCastShadowCallback(IConVar *pVar, const char *pszOldValue, float flOldValue);
static ConVar r_worldlight_castshadows( "r_worldlight_castshadows", "1", FCVAR_CHEAT, "Allow world lights to cast shadows", true, 0, true, 1, WorldLightCastShadowCallback );
static ConVar r_worldlight_lerptime( "r_worldlight_lerptime", "0.5", FCVAR_CHEAT );
static ConVar r_worldlight_debug( "r_worldlight_debug", "0", FCVAR_CHEAT );
static ConVar r_worldlight_shortenfactor( "r_worldlight_shortenfactor", "2" , FCVAR_CHEAT, "Makes shadows cast from local lights shorter" );
static ConVar r_worldlight_mincastintensity( "r_worldlight_mincastintensity", "0.3", FCVAR_CHEAT, "Minimum brightness of a light to be classed as shadow casting", true, 0, false, 0 );

At approximately line 794, replace the line:

void ComputeShadowBBox( IClientRenderable *pRenderable, const Vector &vecAbsCenter, float flRadius, Vector *pAbsMins, Vector *pAbsMaxs );

With:

void ComputeShadowBBox( IClientRenderable *pRenderable, ClientShadowHandle_t shadowHandle, const Vector &vecAbsCenter, float flRadius, Vector *pAbsMins, Vector *pAbsMaxs );

At approximately line 806, above:

private:

Add:

void SetShadowFromWorldLightsEnabled(bool bEnabled);
bool IsShadowingFromWorldLights() const { return m_bShadowFromWorldLights; }

At approximately line 825, below:

Vector2D				m_WorldSize;

Add:

Vector					m_ShadowDir;

At approximately line 829, below:

QAngle					m_LastAngles;

Add:

Vector					m_CurrentLightPos;	// When shadowing from local lights, stores the position of the currently shadowing light
Vector					m_TargetLightPos;	// When shadowing from local lights, stores the position of the new shadowing light
float					m_LightPosLerp;		// Lerp progress when going from current to target light

At approximately line 994, below:

int m_nMaxDepthTextureShadows;

Add:

bool m_bShadowFromWorldLights;

At approximately line 1114, replace the line:

s_ClientShadowMgr.ComputeShadowBBox( pRenderable, vecAbsCenter, flRadius, &vecAbsMins, &vecAbsMaxs );

With:

s_ClientShadowMgr.ComputeShadowBBox( pRenderable, shadow.m_ShadowHandle, vecAbsCenter, flRadius, &vecAbsMins, &vecAbsMaxs );

At approximately line 1194, below:

m_bThreaded = false;

Add:

m_bShadowFromWorldLights = r_worldlight_castshadows.GetBool();

At approximately line 1853, below:

shadow.m_nRenderFrame = -1;

Add:

shadow.m_ShadowDir = GetShadowDirection();
shadow.m_CurrentLightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
shadow.m_TargetLightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
shadow.m_LightPosLerp = FLT_MAX;

At approximately line 2328, replace the line:

Vector vecShadowDir = GetShadowDirection( pRenderable );

With:

Vector vecShadowDir = GetShadowDirection( handle );

At approximately line 2511, replace the line:

Vector vecShadowDir = GetShadowDirection( pRenderable );

With:

Vector vecShadowDir = GetShadowDirection( handle );

At approximately line 2975, replace the lines:

Assert( m_Shadows.IsValidIndex( handle ) );
UpdateProjectedTextureInternal( handle, false );

With:

UpdateDirtyShadow(handle);

At approximately line 3138, replace the line:

if (force || (origin != shadow.m_LastOrigin) || (angles != shadow.m_LastAngles))

With:

if (force || (origin != shadow.m_LastOrigin) || (angles != shadow.m_LastAngles) || shadow.m_LightPosLerp < 1.0f)

At approximately line 3284, replace the line:

void CClientShadowMgr::ComputeShadowBBox( IClientRenderable *pRenderable, const Vector &vecAbsCenter, float flRadius, Vector *pAbsMins, Vector *pAbsMaxs )

With:

void CClientShadowMgr::ComputeShadowBBox( IClientRenderable *pRenderable, ClientShadowHandle_t shadowHandle, const Vector &vecAbsCenter, float flRadius, Vector *pAbsMins, Vector *pAbsMaxs )

At approximately line 3289, replace the line:

Vector vecShadowDir = GetShadowDirection( pRenderable );

With:

Vector vecShadowDir = GetShadowDirection( shadowHandle );

At approximately line 3387, replace the line:

Vector vecShadowDir = GetShadowDirection( pSourceRenderable );

With:

Vector vecShadowDir = GetShadowDirection( handle );

At approximately line 4222, below:

	while( pChild )
	{
		if( pChild->GetClientRenderable()==pRenderable )
			return true;

		pChild = pChild->NextMovePeer();
	}
							
	return false;
}

Add:

const Vector &CClientShadowMgr::GetShadowDirection( ClientShadowHandle_t shadowHandle ) const
{
	Assert( shadowHandle != CLIENTSHADOW_INVALID_HANDLE );

	IClientRenderable* pRenderable = ClientEntityList().GetClientRenderableFromHandle( m_Shadows[shadowHandle].m_Entity );
	Assert( pRenderable );

	if ( !IsShadowingFromWorldLights() )
	{
		return GetShadowDirection( pRenderable );
	}

	Vector &vecResult = AllocTempVector();
	vecResult = m_Shadows[shadowHandle].m_ShadowDir;

	// Allow the renderable to override the default
	pRenderable->GetShadowCastDirection( &vecResult, GetActualShadowCastType( pRenderable ) );

	return vecResult;
}

void CClientShadowMgr::UpdateShadowDirectionFromLocalLightSource( ClientShadowHandle_t shadowHandle )
{
	Assert( shadowHandle != CLIENTSHADOW_INVALID_HANDLE );

	ClientShadow_t& shadow = m_Shadows[shadowHandle];

	IClientRenderable* pRenderable = ClientEntityList().GetClientRenderableFromHandle( shadow.m_Entity );

	// TODO: Figure out why this still gets hit
	Assert( pRenderable );
	if ( !pRenderable )
	{
		DevWarning( "%s(): Skipping shadow with invalid client renderable (shadow handle %d)\n", __FUNCTION__, shadowHandle );
		return;
	}

	Vector bbMin, bbMax;
	pRenderable->GetRenderBoundsWorldspace( bbMin, bbMax );
	Vector origin( 0.5f * ( bbMin + bbMax ) );
	origin.z = bbMin.z;	// Putting origin at the bottom of the bounding box makes the shadows a little shorter

	Vector lightPos;
	Vector lightBrightness;

	if ( shadow.m_LightPosLerp >= 1.0f )	// skip finding new light source if we're in the middle of a lerp
	{
		// Calculate minimum brightness squared
		float flMinBrightnessSqr = r_worldlight_mincastintensity.GetFloat();
		flMinBrightnessSqr *= flMinBrightnessSqr;

		if(g_pWorldLights->GetBrightestLightSource(pRenderable->GetRenderOrigin(), lightPos, lightBrightness) == false ||
			lightBrightness.LengthSqr() < flMinBrightnessSqr )
		{
			// didn't find a light source at all, use default shadow direction
			// TODO: Could switch to using blobby shadow in this case
			lightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
		}
	}

	if ( shadow.m_LightPosLerp == FLT_MAX )	// first light pos ever, just init
	{
		shadow.m_CurrentLightPos = lightPos;
		shadow.m_TargetLightPos = lightPos;
		shadow.m_LightPosLerp = 1.0f;
	}
	else if ( shadow.m_LightPosLerp < 1.0f )
	{
		// We're in the middle of a lerp from current to target light. Finish it.
		shadow.m_LightPosLerp += gpGlobals->frametime * 1.0f/r_worldlight_lerptime.GetFloat();
		shadow.m_LightPosLerp = clamp( shadow.m_LightPosLerp, 0.0f, 1.0f );

		Vector currLightPos( shadow.m_CurrentLightPos );
		Vector targetLightPos( shadow.m_TargetLightPos );
		if ( currLightPos.x == FLT_MAX )
		{
			currLightPos = origin - 200.0f * GetShadowDirection();
		}
		if ( targetLightPos.x == FLT_MAX )
		{
			targetLightPos = origin - 200.0f * GetShadowDirection();
		}

		// lerp light pos
		Vector v1 = origin - shadow.m_CurrentLightPos;
		v1.NormalizeInPlace();

		Vector v2 = origin - shadow.m_TargetLightPos;
		v2.NormalizeInPlace();

		// SAULUNDONE: caused over top sweeping far too often
#if 0
		if ( v1.Dot( v2 ) < 0.0f )
		{
			// if change in shadow angle is more than 90 degrees, lerp over the renderable's top to avoid long sweeping shadows
			Vector fakeOverheadLightPos( origin.x, origin.y, origin.z + 200.0f );
			if( shadow.m_LightPosLerp < 0.5f )
			{
				lightPos = Lerp( 2.0f * shadow.m_LightPosLerp, currLightPos, fakeOverheadLightPos );
			}
			else
			{
				lightPos = Lerp( 2.0f * shadow.m_LightPosLerp - 1.0f, fakeOverheadLightPos, targetLightPos );
			}
		}
		else
#endif
		{
			lightPos = Lerp( shadow.m_LightPosLerp, currLightPos, targetLightPos );
		}

		if ( shadow.m_LightPosLerp >= 1.0f )
		{
			shadow.m_CurrentLightPos = shadow.m_TargetLightPos;
		}
	}
	else if ( shadow.m_LightPosLerp >= 1.0f )
	{
		// check if we have a new closest light position and start a new lerp
		float flDistSq = ( lightPos - shadow.m_CurrentLightPos ).LengthSqr();

		if ( flDistSq > 1.0f )
		{
			// light position has changed, which means we got a new light source. Initiate a lerp
			shadow.m_TargetLightPos = lightPos;
			shadow.m_LightPosLerp = 0.0f;
		}

		lightPos = shadow.m_CurrentLightPos;
	}

	if ( lightPos.x == FLT_MAX )
	{
		lightPos = origin - 200.0f * GetShadowDirection();
	}

	Vector vecResult( origin - lightPos );
	vecResult.NormalizeInPlace();

	vecResult.z *= r_worldlight_shortenfactor.GetFloat();
	vecResult.NormalizeInPlace();

	shadow.m_ShadowDir = vecResult;

	if ( r_worldlight_debug.GetBool() )
	{
		NDebugOverlay::Line( lightPos, origin, 255, 255, 0, false, 0.0f );
	}
}

void CClientShadowMgr::UpdateDirtyShadow( ClientShadowHandle_t handle )
{
	Assert( m_Shadows.IsValidIndex( handle ) );

	if( IsShadowingFromWorldLights() )
		UpdateShadowDirectionFromLocalLightSource( handle );

	UpdateProjectedTextureInternal( handle, false );
}

void WorldLightCastShadowCallback(IConVar *pVar, const char *pszOldValue, float flOldValue)
{
	s_ClientShadowMgr.SetShadowFromWorldLightsEnabled(r_worldlight_castshadows.GetBool());
}

void CClientShadowMgr::SetShadowFromWorldLightsEnabled( bool bEnabled )
{
	if(bEnabled == IsShadowingFromWorldLights())
		return;

	m_bShadowFromWorldLights = bEnabled;
	UpdateAllShadows();
}