Dynamic RTT shadow angles in Source 2007
Introduction
This article explains how to implement dynamic render-to-texture shadow angles in both Source 2007 and Source 2013 mods. Render-to-texture shadows are cast by most non-static renderables (dynamic props, players, NPCs, etc.) in the Source Engine. The direction of the shadow they cast is generally given by the shadow_control entity, but it affects all entities in a level.
In Left 4 Dead, Portal 2, Alien Swarm, Dark Messiah of Might and Magic and Mapbase, the direction of a shadow is calculated on a per-entity basis, and dictated by the closest light to the entity. By using parts of the Alien Swarm codebase in your mod, along with some custom files (see below), it is possible to implement this feature in Source 2007 as well as Source 2013.
game/client/worldlight.cpp
Create a new file in your /game/client folder called worldlight.cpp, and paste the following code into it.
//========= Copyright (C) 2021, CSProMod Team, All rights reserved. =========//
//
// Purpose: Provide world light-related functions to the client
//
// As the engine provides no access to brush/model data (brushdata_t, model_t),
// we hence have no access to dworldlight_t. Therefore, we manually extract the
// world light data from the BSP itself, before entities are initialized on map
// load.
//
// To find the brightest light at a point, all world lights are iterated.
// Lights whose radii do not encompass our sample point are quickly rejected,
// as are lights that are not in our PVS, or visible from the sample point.
// If the sky light is visible from the sample point, then it shall supersede
// all other world lights.
//
// Written: November 2011
// Author: Saul Rennison
//
//===========================================================================//
#include "cbase.h"
#include "bspfile.h"
#include "client_factorylist.h" // FactoryList_Retrieve
#include "eiface.h" // IVEngineServer
#include "filesystem.h"
#include "worldlight.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
static IVEngineServer *g_pEngineServer = nullptr;
//-----------------------------------------------------------------------------
// Purpose: Calculate intensity ratio for a worldlight by distance
//-----------------------------------------------------------------------------
static float Engine_WorldLightDistanceFalloff( const dworldlight_t *wl, const Vector &delta )
{
float falloff;
switch ( wl->type )
{
case emit_surface:
// Cull out stuff that's too far
if ( wl->radius != 0 )
{
if ( DotProduct( delta, delta ) > ( wl->radius * wl->radius ) )
return 0.0f;
}
return InvRSquared( delta );
case emit_skylight:
return 1.0f;
case emit_quakelight:
// X - r;
falloff = wl->linear_attn - FastSqrt( DotProduct( delta, delta ) );
if ( falloff < 0 )
return 0.0f;
return falloff;
case emit_skyambient:
return 1.0f;
case emit_point:
case emit_spotlight: // Directional & positional
{
float dist2, dist;
dist2 = DotProduct( delta, delta );
dist = FastSqrt( dist2 );
// Cull out stuff that's too far
if ( wl->radius != 0 && dist > wl->radius )
return 0.0f;
return 1.0f / ( wl->constant_attn + wl->linear_attn * dist + wl->quadratic_attn * dist2 );
}
}
return 1.0f;
}
//-----------------------------------------------------------------------------
// Purpose: Initialize game system and members
//-----------------------------------------------------------------------------
CWorldLights::CWorldLights() : CAutoGameSystem( "World lights" )
{
m_nWorldLights = 0;
m_pWorldLights = nullptr;
}
//-----------------------------------------------------------------------------
// Purpose: Clear worldlights, free memory
//-----------------------------------------------------------------------------
void CWorldLights::Clear()
{
m_nWorldLights = 0;
if ( m_pWorldLights )
{
delete[] m_pWorldLights;
m_pWorldLights = nullptr;
}
}
//-----------------------------------------------------------------------------
// Purpose: Get the IVEngineServer, we need this for the PVS functions
//-----------------------------------------------------------------------------
bool CWorldLights::Init()
{
factorylist_t factories;
FactoryList_Retrieve( factories );
if ( ( g_pEngineServer = static_cast<IVEngineServer *>( factories.appSystemFactory( INTERFACEVERSION_VENGINESERVER, nullptr ) ) ) == nullptr )
return false;
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Get all world lights from the BSP
//-----------------------------------------------------------------------------
void CWorldLights::LevelInitPreEntity()
{
// Get the map path
const char *pszMapName = modelinfo->GetModelName( modelinfo->GetModel( 1 ) );
// Open map
FileHandle_t hFile = g_pFullFileSystem->Open( pszMapName, "rb" );
if ( !hFile )
{
Warning( "CWorldLights: Unable to open map\n" );
return;
}
// Read the BSP header. We don't need to do any version checks, etc. as we
// can safely assume that the engine did this for us
dheader_t hdr;
g_pFullFileSystem->Read( &hdr, sizeof( hdr ), hFile );
// Grab the light lump and seek to it
lump_t &lightLump = hdr.lumps[LUMP_WORLDLIGHTS];
// If the worldlights lump is empty, that means theres no normal, LDR lights to extract
// This can happen when, for example, the map is compiled in HDR mode only
// So move on to the HDR worldlights lump
if ( lightLump.filelen == 0 )
{
lightLump = hdr.lumps[LUMP_WORLDLIGHTS_HDR];
}
// If we can't divide the lump data into a whole number of worldlights,
// then the BSP format changed and we're unaware
if ( lightLump.filelen % sizeof( dworldlight_t ) )
{
Warning( "CWorldLights: Unknown world light lump\n" );
// Close file
g_pFullFileSystem->Close( hFile );
return;
}
g_pFullFileSystem->Seek( hFile, lightLump.fileofs, FILESYSTEM_SEEK_HEAD );
// Allocate memory for the worldlights
m_nWorldLights = lightLump.filelen / sizeof( dworldlight_t );
m_pWorldLights = new dworldlight_t[m_nWorldLights];
// Read worldlights then close
g_pFullFileSystem->Read( m_pWorldLights, lightLump.filelen, hFile );
g_pFullFileSystem->Close( hFile );
DevMsg( "CWorldLights: Load successful (%d lights at 0x%p)\n", m_nWorldLights, m_pWorldLights );
}
//-----------------------------------------------------------------------------
// Purpose: Find the brightest light source at a point
//-----------------------------------------------------------------------------
bool CWorldLights::GetBrightestLightSource( const Vector &vecPosition, Vector &vecLightPos, Vector &vecLightBrightness )
{
if ( !m_nWorldLights || !m_pWorldLights )
return false;
// Default light position and brightness to zero
vecLightBrightness.Init();
vecLightPos.Init();
// Find the size of the PVS for our current position
int nCluster = g_pEngineServer->GetClusterForOrigin( vecPosition );
int nPVSSize = g_pEngineServer->GetPVSForCluster( nCluster, 0, nullptr );
// Get the PVS at our position
byte *pvs = new byte[nPVSSize];
g_pEngineServer->GetPVSForCluster( nCluster, nPVSSize, pvs );
// Iterate through all the worldlights
for ( int i = 0; i < m_nWorldLights; ++i )
{
dworldlight_t *light = &m_pWorldLights[i];
// Skip skyambient
if ( light->type == emit_skyambient )
{
//engine->Con_NPrintf( i, "%d: skyambient", i );
continue;
}
// Handle sun
if ( light->type == emit_skylight )
{
// Calculate sun position
Vector vecAbsStart = vecPosition + Vector( 0, 0, 30 );
Vector vecAbsEnd = vecAbsStart - ( light->normal * MAX_TRACE_LENGTH );
trace_t tr;
UTIL_TraceLine( vecPosition, vecAbsEnd, MASK_OPAQUE, nullptr, COLLISION_GROUP_NONE, &tr );
// If we didn't hit anything then we have a problem
if ( !tr.DidHit() )
{
//engine->Con_NPrintf( i, "%d: skylight: couldn't touch sky", i );
continue;
}
// If we did hit something, and it wasn't the skybox, then skip
// this worldlight
if ( !( tr.surface.flags & SURF_SKY ) && !( tr.surface.flags & SURF_SKY2D ) )
{
//engine->Con_NPrintf( i, "%d: skylight: no sight to sun", i );
continue;
}
// Act like we didn't find any valid worldlights, so the shadow
// manager uses the default shadow direction instead (should be the
// sun direction)
delete[] pvs;
return false;
}
// Calculate square distance to this worldlight
Vector vecDelta = light->origin - vecPosition;
float flDistSqr = vecDelta.LengthSqr();
float flRadiusSqr = light->radius * light->radius;
// Skip lights that are out of our radius
if ( flRadiusSqr > 0 && flDistSqr >= flRadiusSqr )
{
//engine->Con_NPrintf( i, "%d: out-of-radius (dist: %d, radius: %d)", i, sqrt( flDistSqr ), light->radius );
continue;
}
// Is it out of our PVS?
if ( !g_pEngineServer->CheckOriginInPVS( light->origin, pvs, nPVSSize ) )
{
//engine->Con_NPrintf( i, "%d: out of PVS", i );
continue;
}
// Calculate intensity at our position
float flRatio = Engine_WorldLightDistanceFalloff( light, vecDelta );
Vector vecIntensity = light->intensity * flRatio;
// Is this light more intense than the one we already found?
if ( vecIntensity.LengthSqr() <= vecLightBrightness.LengthSqr() )
{
//engine->Con_NPrintf( i, "%d: too dim", i );
continue;
}
// Can we see the light?
trace_t tr;
Vector vecAbsStart = vecPosition + Vector( 0, 0, 30 );
UTIL_TraceLine( vecAbsStart, light->origin, MASK_OPAQUE, nullptr, COLLISION_GROUP_NONE, &tr );
if ( tr.DidHit() )
{
//engine->Con_NPrintf( i, "%d: trace failed", i );
continue;
}
vecLightPos = light->origin;
vecLightBrightness = vecIntensity;
//engine->Con_NPrintf( i, "%d: set (%.2f)", i, vecIntensity.Length() );
}
delete[] pvs;
//engine->Con_NPrintf( m_nWorldLights, "result: %d", !vecLightBrightness.IsZero() );
return !vecLightBrightness.IsZero();
}
//-----------------------------------------------------------------------------
// Singleton accessor
//-----------------------------------------------------------------------------
static CWorldLights s_WorldLights;
CWorldLights *g_pWorldLights = &s_WorldLights;
game/client/worldlight.h
Now create worldlight.h within /game/client, pasting in the following code:
//========= Copyright (C) 2021, CSProMod Team, All rights reserved. =========//
//
// Purpose: Provide world light-related functions to the client
//
// Written: November 2011
// Author: Saul Rennison
//
//===========================================================================//
#ifndef WORLDLIGHT_H
#define WORLDLIGHT_H
#ifdef _WIN32
#pragma once
#endif
#include "igamesystem.h" // CAutoGameSystem
class Vector;
struct dworldlight_t;
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
class CWorldLights : public CAutoGameSystem
{
public:
CWorldLights();
~CWorldLights() { Clear(); }
bool GetBrightestLightSource( const Vector &vecPosition, Vector &vecLightPos, Vector &vecLightBrightness );
// CAutoGameSystem overrides
bool Init() OVERRIDE;
void LevelInitPreEntity() OVERRIDE;
void LevelShutdownPostEntity() OVERRIDE { Clear(); }
private:
void Clear();
private:
int m_nWorldLights;
dworldlight_t *m_pWorldLights;
};
// Singleton accessor
extern CWorldLights *g_pWorldLights;
#endif // WORLDLIGHT_H
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(s) 803, below:
void SetShadowsDisabled( bool bDisabled )
{
r_shadows_gamecontrol.SetValue( bDisabled != 1 );
}
Add:
void SuppressShadowFromWorldLights( bool bSuppress );
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 931, below:
const Vector &GetShadowDirection( IClientRenderable *pRenderable ) const;
Add:
const Vector &GetShadowDirection( ClientShadowHandle_t shadowHandle ) const;
At approximately line 966, below:
void SetViewFlashlightState( int nActiveFlashlightCount, ClientShadowHandle_t* pActiveFlashlights );
Add:
void UpdateDirtyShadow( ClientShadowHandle_t handle );
void UpdateShadowDirectionFromLocalLightSource( ClientShadowHandle_t shadowHandle );
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:
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
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;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
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 );
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CClientShadowMgr::UpdateDirtyShadow( ClientShadowHandle_t handle )
{
Assert( m_Shadows.IsValidIndex( handle ) );
if ( IsShadowingFromWorldLights() )
UpdateShadowDirectionFromLocalLightSource( handle );
UpdateProjectedTextureInternal( handle, false );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void WorldLightCastShadowCallback( IConVar *pVar, const char *pszOldValue, float flOldValue )
{
s_ClientShadowMgr.SetShadowFromWorldLightsEnabled( r_worldlight_castshadows.GetBool() );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CClientShadowMgr::SetShadowFromWorldLightsEnabled( bool bEnabled )
{
if ( bEnabled == IsShadowingFromWorldLights() )
return;
m_bShadowFromWorldLights = bEnabled;
UpdateAllShadows();
}
Complete source code of clientshadowmgr.cpp
See Dynamic RTT shadow angles in Source 2007/Clientshadowmgr.cpp.
Conclusion
Now recompile, and dynamically angled shadows should be enabled in your mod.
Compile errors or incorrect functionality may arise from:
- Not including the worldlight.cpp/h files in your client project.
- Missing steps. Specifically, double check your clientshadowmgr.cpp changes.
Happy modding!