Dynamic RTT shadow angles in Source 2007
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();
}