Dynamic RTT shadow angles in Source 2007: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
m (Fixed memory leak)
 
(23 intermediate revisions by 9 users not shown)
Line 1: Line 1:
{{LanguageBar|title=Dynamic RTT shadow angles in Source 2007 and Source 2013}}
[[File:Dynamic_rtt.jpg|thumb|The difference between a mod with this code(shadows facing away from light) and a mod/game without it(shadows directly under entities).]]
[[File:Dynamic_rtt.jpg|thumb|The difference between a mod with this code(shadows facing away from light) and a mod/game without it(shadows directly under entities).]]
==Introduction==
This article explains how to implement dynamic [[Dynamic Shadows|render-to-texture]] shadow angles in both {{src07|1}}, {{src13|1}} and {{tf2branch|1}} mods. Render-to-texture shadows are cast by most non-static [[Entity|renderables]] (dynamic props, players, NPCs, etc.) in the Source Engine. The direction of the shadow they cast is generally given by the {{ent|shadow_control}} entity, but it affects all entities in a level.


==Introduction==
In {{l4d|4}}, {{portal2|4}}, {{as|4}}, {{dmmm|4}} and {{mapbase|4}}, 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 {{src07|1}}, {{src13|1}} as well as {{tf2branch|1}}.  
This article explains how to implement dynamic [[Dynamic Shadows|render-to-texture]] shadow angles in [[Source 2007]] mods. Render-to-texture shadows are cast by most non-static [[Entity|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]] 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 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]].  
{{bug|hidetested=1|This dynamic shadow angle implementation doesn't work correctly, the shadows have lerping issues when standing in between 2 lights.}}


===game/client/worldlight.cpp===
===game/client/worldlight.cpp===
Create a new file in your '''/game/client''' folder called '''worldlight.cpp''', and paste the following code into it.
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.}}
{{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.}}


<source lang="cpp">
<source lang="cpp">
//========= Copyright (C) 2011, CSProMod Team, All rights reserved. =========//
//========= Copyright (C) 2021, CSProMod Team, All rights reserved. =========//
//
//
// Purpose: provide world light related functions to the client
// Purpose: Provide world light-related functions to the client
//
//
// As the engine provides no access to brush/model data (brushdata_t, model_t),
// 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
// we hence have no access to dworldlight_t. Therefore, we manually extract the
// world light data from the BSP itself, before entities are initialised on map
// world light data from the BSP itself, before entities are initialized on map
// load.
// load.
//
//
// To find the brightest light at a point, all world lights are iterated.
// 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,
// Lights whose radii do not encompass our sample point are quickly rejected,
// as are lights which are not in our PVS, or visible from the sample point.
// 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
// If the sky light is visible from the sample point, then it shall supersede
// all other world lights.
// all other world lights.
Line 33: Line 35:


#include "cbase.h"
#include "cbase.h"
#include "worldlight.h"
#include "bspfile.h"
#include "bspfile.h"
#include "filesystem.h"
#include "client_factorylist.h" // FactoryList_Retrieve
#include "client_factorylist.h" // FactoryList_Retrieve
#include "eiface.h" // IVEngineServer
#include "eiface.h" // IVEngineServer
#include "filesystem.h"
#include "worldlight.h"


static IVEngineServer *g_pEngineServer = NULL;
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"


//-----------------------------------------------------------------------------
static IVEngineServer *g_pEngineServer = nullptr;
// Singleton exposure
//-----------------------------------------------------------------------------
static CWorldLights s_WorldLights;
CWorldLights *g_pWorldLights = &s_WorldLights;


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Purpose: calculate intensity ratio for a worldlight by distance
// Purpose: Calculate intensity ratio for a worldlight by distance
// Author: Valve Software
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
static float Engine_WorldLightDistanceFalloff( const dworldlight_t *wl, const Vector& delta )
static float Engine_WorldLightDistanceFalloff( const dworldlight_t *wl, const Vector &delta )
{
{
float falloff;
float falloff;


switch (wl->type)
switch ( wl->type )
{
{
case emit_surface:
case emit_surface:
// Cull out stuff that's too far
// Cull out stuff that's too far
if(wl->radius != 0)
if ( wl->radius != 0 )
{
{
if(DotProduct( delta, delta ) > (wl->radius * wl->radius))
if ( DotProduct( delta, delta ) > ( wl->radius * wl->radius ) )
return 0.0f;
return 0.0f;
}
}


return InvRSquared(delta);
return InvRSquared( delta );
break;


case emit_skylight:
case emit_skylight:
return 1.f;
return 1.0f;
break;


case emit_quakelight:
case emit_quakelight:
// X - r;
// X - r;
falloff = wl->linear_attn - FastSqrt( DotProduct( delta, delta ) );
falloff = wl->linear_attn - FastSqrt( DotProduct( delta, delta ) );
if(falloff < 0)
if ( falloff < 0 )
return 0.f;
return 0.0f;


return falloff;
return falloff;
break;


case emit_skyambient:
case emit_skyambient:
return 1.f;
return 1.0f;
break;


case emit_point:
case emit_point:
case emit_spotlight: // directional & positional
case emit_spotlight: // Directional & positional
{
{
float dist2, dist;
float dist2, dist;


dist2 = DotProduct(delta, delta);
dist2 = DotProduct( delta, delta );
dist = FastSqrt(dist2);
dist = FastSqrt( dist2 );


// Cull out stuff that's too far
// Cull out stuff that's too far
if(wl->radius != 0 && dist > wl->radius)
if ( wl->radius != 0 && dist > wl->radius )
return 0.f;
return 0.0f;


return 1.f / (wl->constant_attn + wl->linear_attn * dist + wl->quadratic_attn * dist2);
return 1.0f / ( wl->constant_attn + wl->linear_attn * dist + wl->quadratic_attn * dist2 );
}
}
break;
}
}


return 1.f;
return 1.0f;
}
}


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Purpose: initialise game system and members
// Purpose: Initialize game system and members
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
CWorldLights::CWorldLights() : CAutoGameSystem("World lights")
CWorldLights::CWorldLights() : CAutoGameSystem( "World lights" )
{
{
m_nWorldLights = 0;
m_nWorldLights = 0;
m_pWorldLights = NULL;
m_pWorldLights = nullptr;
}
}


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Purpose: clear worldlights, free memory
// Purpose: Clear worldlights, free memory
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CWorldLights::Clear()
void CWorldLights::Clear()
Line 122: Line 114:
m_nWorldLights = 0;
m_nWorldLights = 0;


if(m_pWorldLights)
if ( m_pWorldLights )
{
{
delete [] m_pWorldLights;
delete[] m_pWorldLights;
m_pWorldLights = NULL;
m_pWorldLights = nullptr;
}
}
}
}


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Purpose: get the IVEngineServer, we need this for the PVS functions
// Purpose: Get the IVEngineServer, we need this for the PVS functions
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CWorldLights::Init()
bool CWorldLights::Init()
{
{
factorylist_t factories;
factorylist_t factories;
FactoryList_Retrieve(factories);
FactoryList_Retrieve( factories );


if((g_pEngineServer = (IVEngineServer*)factories.appSystemFactory(INTERFACEVERSION_VENGINESERVER, NULL)) == NULL)
if ( ( g_pEngineServer = static_cast<IVEngineServer *>( factories.appSystemFactory( INTERFACEVERSION_VENGINESERVER, nullptr ) ) ) == nullptr )
return false;
return false;


Line 144: Line 136:


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Purpose: get all world lights from the BSP
// Purpose: Get all world lights from the BSP
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CWorldLights::LevelInitPreEntity()
void CWorldLights::LevelInitPreEntity()
{
{
// Get the map path
// Get the map path
const char *pszMapName = modelinfo->GetModelName(modelinfo->GetModel(1));
const char *pszMapName = modelinfo->GetModelName( modelinfo->GetModel( 1 ) );


// Open map
// Open map
FileHandle_t hFile = g_pFullFileSystem->Open(pszMapName, "rb");
FileHandle_t hFile = g_pFullFileSystem->Open( pszMapName, "rb" );
if(!hFile)
if ( !hFile )
{
{
Warning("CWorldLights: unable to open map\n");
Warning( "CWorldLights: Unable to open map\n" );
return;
return;
}
}
Line 162: Line 154:
// can safely assume that the engine did this for us
// can safely assume that the engine did this for us
dheader_t hdr;
dheader_t hdr;
g_pFullFileSystem->Read(&hdr, sizeof(hdr), hFile);
g_pFullFileSystem->Read( &hdr, sizeof( hdr ), hFile );


// Grab the light lump and seek to it
// Grab the light lump and seek to it
lump_t &lightLump = hdr.lumps[LUMP_WORLDLIGHTS];
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,
// If we can't divide the lump data into a whole number of worldlights,
// then the BSP format changed and we're unaware
// then the BSP format changed and we're unaware
if(lightLump.filelen % sizeof(dworldlight_t))
if ( lightLump.filelen % sizeof( dworldlight_t ) )
{
{
Warning("CWorldLights: unknown world light lump\n");
Warning( "CWorldLights: Unknown world light lump\n" );


// Close file
// Close file
g_pFullFileSystem->Close(hFile);
g_pFullFileSystem->Close( hFile );
return;
return;
}
}


g_pFullFileSystem->Seek(hFile, lightLump.fileofs, FILESYSTEM_SEEK_HEAD);
g_pFullFileSystem->Seek( hFile, lightLump.fileofs, FILESYSTEM_SEEK_HEAD );


// Allocate memory for the worldlights
// Allocate memory for the worldlights
m_nWorldLights = lightLump.filelen / sizeof(dworldlight_t);
m_nWorldLights = lightLump.filelen / sizeof( dworldlight_t );
m_pWorldLights = new dworldlight_t[m_nWorldLights];
m_pWorldLights = new dworldlight_t[m_nWorldLights];


// Read worldlights then close
// Read worldlights then close
g_pFullFileSystem->Read(m_pWorldLights, lightLump.filelen, hFile);
g_pFullFileSystem->Read( m_pWorldLights, lightLump.filelen, hFile );
g_pFullFileSystem->Close(hFile);
g_pFullFileSystem->Close( hFile );


DevMsg("CWorldLights: load successful (%d lights at 0x%p)\n", m_nWorldLights, m_pWorldLights);
DevMsg( "CWorldLights: Load successful (%d lights at 0x%p)\n", m_nWorldLights, m_pWorldLights );
}
}


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Purpose: find the brightest light source at a point
// Purpose: Find the brightest light source at a point
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CWorldLights::GetBrightestLightSource(const Vector &vecPosition, Vector &vecLightPos, Vector &vecLightBrightness)
bool CWorldLights::GetBrightestLightSource( const Vector &vecPosition, Vector &vecLightPos, Vector &vecLightBrightness )
{
{
if(!m_nWorldLights || !m_pWorldLights)
if ( !m_nWorldLights || !m_pWorldLights )
return false;
return false;


Line 204: Line 204:


// Find the size of the PVS for our current position
// Find the size of the PVS for our current position
int nCluster = g_pEngineServer->GetClusterForOrigin(vecPosition);
int nCluster = g_pEngineServer->GetClusterForOrigin( vecPosition );
int nPVSSize = g_pEngineServer->GetPVSForCluster(nCluster, 0, NULL);
int nPVSSize = g_pEngineServer->GetPVSForCluster( nCluster, 0, nullptr );


// Get the PVS at our position
// Get the PVS at our position
byte *pvs = new byte[nPVSSize];
byte *pvs = new byte[nPVSSize];
g_pEngineServer->GetPVSForCluster(nCluster, nPVSSize, pvs);
g_pEngineServer->GetPVSForCluster( nCluster, nPVSSize, pvs );


// Iterate through all the worldlights
// Iterate through all the worldlights
for(int i = 0; i < m_nWorldLights; ++i)
for ( int i = 0; i < m_nWorldLights; ++i )
{
{
dworldlight_t *light = &m_pWorldLights[i];
dworldlight_t *light = &m_pWorldLights[i];


// Skip skyambient
// Skip skyambient
if(light->type == emit_skyambient)
if ( light->type == emit_skyambient )
{
{
//engine->Con_NPrintf(i, "%d: skyambient", i);
//engine->Con_NPrintf( i, "%d: skyambient", i );
continue;
continue;
}
}


// Handle sun
// Handle sun
if(light->type == emit_skylight)
if ( light->type == emit_skylight )
{
{
// Calculate sun position
// Calculate sun position
Vector vecAbsStart = vecPosition + Vector(0,0,30);
Vector vecAbsStart = vecPosition + Vector( 0, 0, 30 );
Vector vecAbsEnd = vecAbsStart - (light->normal * MAX_TRACE_LENGTH);
Vector vecAbsEnd = vecAbsStart - ( light->normal * MAX_TRACE_LENGTH );


trace_t tr;
trace_t tr;
UTIL_TraceLine(vecPosition, vecAbsEnd, MASK_OPAQUE, NULL, COLLISION_GROUP_NONE, &tr);
UTIL_TraceLine( vecPosition, vecAbsEnd, MASK_OPAQUE, nullptr, COLLISION_GROUP_NONE, &tr );


// If we didn't hit anything then we have a problem
// If we didn't hit anything then we have a problem
if(!tr.DidHit())
if ( !tr.DidHit() )
{
{
//engine->Con_NPrintf(i, "%d: skylight: couldn't touch sky", i);
//engine->Con_NPrintf( i, "%d: skylight: couldn't touch sky", i );
continue;
continue;
}
}
Line 242: Line 242:
// If we did hit something, and it wasn't the skybox, then skip
// If we did hit something, and it wasn't the skybox, then skip
// this worldlight
// this worldlight
if(!(tr.surface.flags & SURF_SKY) && !(tr.surface.flags & SURF_SKY2D))
if ( !( tr.surface.flags & SURF_SKY ) && !( tr.surface.flags & SURF_SKY2D ) )
{
{
//engine->Con_NPrintf(i, "%d: skylight: no sight to sun", i);
//engine->Con_NPrintf( i, "%d: skylight: no sight to sun", i );
continue;
continue;
}
}
Line 263: Line 263:


// Skip lights that are out of our radius
// Skip lights that are out of our radius
if(flRadiusSqr > 0 && flDistSqr >= flRadiusSqr)
if ( flRadiusSqr > 0 && flDistSqr >= flRadiusSqr )
{
{
//engine->Con_NPrintf(i, "%d: out-of-radius (dist: %d, radius: %d)", i, sqrt(flDistSqr), light->radius);
//engine->Con_NPrintf( i, "%d: out-of-radius (dist: %d, radius: %d)", i, sqrt( flDistSqr ), light->radius );
continue;
continue;
}
}


// Is it out of our PVS?
// Is it out of our PVS?
if(!g_pEngineServer->CheckOriginInPVS(light->origin, pvs, nPVSSize))
if ( !g_pEngineServer->CheckOriginInPVS( light->origin, pvs, nPVSSize ) )
{
{
//engine->Con_NPrintf(i, "%d: out of PVS", i);
//engine->Con_NPrintf( i, "%d: out of PVS", i );
continue;
continue;
}
}


// Calculate intensity at our position
// Calculate intensity at our position
float flRatio = Engine_WorldLightDistanceFalloff(light, vecDelta);
float flRatio = Engine_WorldLightDistanceFalloff( light, vecDelta );
Vector vecIntensity = light->intensity * flRatio;
Vector vecIntensity = light->intensity * flRatio;


// Is this light more intense than the one we already found?
// Is this light more intense than the one we already found?
if(vecIntensity.LengthSqr() <= vecLightBrightness.LengthSqr())
if ( vecIntensity.LengthSqr() <= vecLightBrightness.LengthSqr() )
{
{
//engine->Con_NPrintf(i, "%d: too dim", i);
//engine->Con_NPrintf( i, "%d: too dim", i );
continue;
continue;
}
}
Line 289: Line 289:
// Can we see the light?
// Can we see the light?
trace_t tr;
trace_t tr;
Vector vecAbsStart = vecPosition + Vector(0,0,30);
Vector vecAbsStart = vecPosition + Vector( 0, 0, 30 );
UTIL_TraceLine(vecAbsStart, light->origin, MASK_OPAQUE, NULL, COLLISION_GROUP_NONE, &tr);
UTIL_TraceLine( vecAbsStart, light->origin, MASK_OPAQUE, nullptr, COLLISION_GROUP_NONE, &tr );


if(tr.DidHit())
if ( tr.DidHit() )
{
{
//engine->Con_NPrintf(i, "%d: trace failed", i);
//engine->Con_NPrintf( i, "%d: trace failed", i );
continue;
continue;
}
}
Line 301: Line 301:
vecLightBrightness = vecIntensity;
vecLightBrightness = vecIntensity;


//engine->Con_NPrintf(i, "%d: set (%.2f)", i, vecIntensity.Length());
//engine->Con_NPrintf( i, "%d: set (%.2f)", i, vecIntensity.Length() );
}
}


delete[] pvs;
delete[] pvs;


//engine->Con_NPrintf(m_nWorldLights, "result: %d", !vecLightBrightness.IsZero());
//engine->Con_NPrintf( m_nWorldLights, "result: %d", !vecLightBrightness.IsZero() );
return !vecLightBrightness.IsZero();
return !vecLightBrightness.IsZero();
}
}
//-----------------------------------------------------------------------------
// Singleton accessor
//-----------------------------------------------------------------------------
static CWorldLights s_WorldLights;
CWorldLights *g_pWorldLights = &s_WorldLights;
</source>
</source>


Line 314: Line 320:
Now create '''worldlight.h''' within '''/game/client''', pasting in the following code:
Now create '''worldlight.h''' within '''/game/client''', pasting in the following code:


{{Note|don’t forget to add these files to your client project}}
{{Note|Don’t forget to add these files to your client project}}


<source lang="cpp">
<source lang="cpp">
//========= Copyright (C) 2011, CSProMod Team, All rights reserved. =========//
//========= Copyright (C) 2021, CSProMod Team, All rights reserved. =========//
//
// Purpose: Provide world light-related functions to the client
//
//
// Purpose: provide world light related functions to the client
//
// Written: November 2011
// Written: November 2011
// Author: Saul Rennison
// Author: Saul Rennison
Line 326: Line 332:
//===========================================================================//
//===========================================================================//


#ifndef WORLDLIGHT_H
#define WORLDLIGHT_H
#ifdef _WIN32
#pragma once
#pragma once
#endif


#include "igamesystem.h" // CAutoGameSystem
#include "igamesystem.h" // CAutoGameSystem
Line 342: Line 352:
~CWorldLights() { Clear(); }
~CWorldLights() { Clear(); }


//-------------------------------------------------------------------------
bool GetBrightestLightSource( const Vector &vecPosition, Vector &vecLightPos, Vector &vecLightBrightness );
// Find the brightest light source at a point
//-------------------------------------------------------------------------
bool GetBrightestLightSource(const Vector &vecPosition, Vector &vecLightPos, Vector &vecLightBrightness);


// CAutoGameSystem overrides
// CAutoGameSystem overrides
public:
bool Init() OVERRIDE;
virtual bool Init();
void LevelInitPreEntity() OVERRIDE;
virtual void LevelInitPreEntity();
void LevelShutdownPostEntity() OVERRIDE { Clear(); }
virtual void LevelShutdownPostEntity() { Clear(); }


private:
private:
void Clear();
void Clear();


private:
int m_nWorldLights;
int m_nWorldLights;
dworldlight_t *m_pWorldLights;
dworldlight_t *m_pWorldLights;
};
};


//-----------------------------------------------------------------------------
// Singleton accessor
// Singleton exposure
//-----------------------------------------------------------------------------
extern CWorldLights *g_pWorldLights;
extern CWorldLights *g_pWorldLights;
#endif // WORLDLIGHT_H
</source>
</source>


Line 381: Line 388:
<source lang="cpp">static ConVar r_flashlight_version2( "r_flashlight_version2", "0" );</source>
<source lang="cpp">static ConVar r_flashlight_version2( "r_flashlight_version2", "0" );</source>
Add:
Add:
<source lang="cpp">void WorldLightCastShadowCallback(IConVar *pVar, const char *pszOldValue, float flOldValue);
<source lang="cpp">
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_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_lerptime( "r_worldlight_lerptime", "0.5", FCVAR_CHEAT );
static ConVar r_worldlight_debug( "r_worldlight_debug", "0", 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_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 );</source>
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 );
</source>


At approximately line 794, replace the line:
At approximately line 794, replace the line:
Line 394: Line 403:


At approximately line(s) 803, below:
At approximately line(s) 803, below:
<source lang="cpp">void SetShadowsDisabled( bool bDisabled )  
<source lang="cpp">
void SetShadowsDisabled( bool bDisabled )  
{  
{  
r_shadows_gamecontrol.SetValue( bDisabled != 1 );
r_shadows_gamecontrol.SetValue( bDisabled != 1 );
}
}
</source>
</source>
Line 407: Line 417:


At approximately line 825, below:
At approximately line 825, below:
<source lang="cpp">Vector2D m_WorldSize; </source>
<source lang="cpp">Vector2D m_WorldSize;</source>
Add:
Add:
<source lang="cpp">Vector m_ShadowDir; </source>
<source lang="cpp">Vector m_ShadowDir;</source>


At approximately line 829, below:
At approximately line 829, below:
<source lang="cpp">QAngle m_LastAngles; </source>
<source lang="cpp">QAngle m_LastAngles;</source>
Add:
Add:
<source lang="cpp">Vector m_CurrentLightPos; // When shadowing from local lights, stores the position of the currently shadowing light
<source lang="cpp">
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
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</source>
float m_LightPosLerp; // Lerp progress when going from current to target light
</source>


At approximately line 931, below:
At approximately line 931, below:
Line 447: Line 459:
<source lang="cpp">shadow.m_nRenderFrame = -1;</source>
<source lang="cpp">shadow.m_nRenderFrame = -1;</source>
Add:
Add:
<source lang="cpp">shadow.m_ShadowDir = GetShadowDirection();
<source lang="cpp">
shadow.m_ShadowDir = GetShadowDirection();
shadow.m_CurrentLightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
shadow.m_CurrentLightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
shadow.m_TargetLightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
shadow.m_TargetLightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
shadow.m_LightPosLerp = FLT_MAX;</source>
shadow.m_LightPosLerp = FLT_MAX;
</source>


At approximately line 2328, replace the line:
At approximately line 2328, replace the line:
Line 463: Line 477:


At approximately line 2975, replace the lines:
At approximately line 2975, replace the lines:
<source lang="cpp">Assert( m_Shadows.IsValidIndex( handle ) );
<source lang="cpp">
UpdateProjectedTextureInternal( handle, false );</source>
Assert( m_Shadows.IsValidIndex( handle ) );
UpdateProjectedTextureInternal( handle, false );
</source>
With:
With:
<source lang="cpp">UpdateDirtyShadow(handle);</source>
<source lang="cpp">UpdateDirtyShadow( handle );</source>


At approximately line 3138, replace the line:
At approximately line 3138, replace the line:
<source lang="cpp">if (force || (origin != shadow.m_LastOrigin) || (angles != shadow.m_LastAngles))</source>
<source lang="cpp">if (force || (origin != shadow.m_LastOrigin) || (angles != shadow.m_LastAngles))</source>
With:
With:
<source lang="cpp">if (force || (origin != shadow.m_LastOrigin) || (angles != shadow.m_LastAngles) || shadow.m_LightPosLerp < 1.0f)</source>
<source lang="cpp">if ( force || (origin != shadow.m_LastOrigin) || (angles != shadow.m_LastAngles) || shadow.m_LightPosLerp < 1.0f )</source>


At approximately line 3284, replace the line:
At approximately line 3284, replace the line:
<source lang="cpp">void CClientShadowMgr::ComputeShadowBBox( IClientRenderable *pRenderable, const Vector &vecAbsCenter, float flRadius, Vector *pAbsMins, Vector *pAbsMaxs ) </source>
<source lang="cpp">void CClientShadowMgr::ComputeShadowBBox( IClientRenderable *pRenderable, const Vector &vecAbsCenter, float flRadius, Vector *pAbsMins, Vector *pAbsMaxs )</source>
With:
With:
<source lang="cpp">void CClientShadowMgr::ComputeShadowBBox( IClientRenderable *pRenderable, ClientShadowHandle_t shadowHandle, const Vector &vecAbsCenter, float flRadius, Vector *pAbsMins, Vector *pAbsMaxs ) </source>
<source lang="cpp">void CClientShadowMgr::ComputeShadowBBox( IClientRenderable *pRenderable, ClientShadowHandle_t shadowHandle, const Vector &vecAbsCenter, float flRadius, Vector *pAbsMins, Vector *pAbsMaxs )</source>


At approximately line 3289, replace the line:
At approximately line 3289, replace the line:
<source lang="cpp">Vector vecShadowDir = GetShadowDirection( pRenderable ); </source>
<source lang="cpp">Vector vecShadowDir = GetShadowDirection( pRenderable );</source>
With:
With:
<source lang="cpp">Vector vecShadowDir = GetShadowDirection( shadowHandle ); </source>
<source lang="cpp">Vector vecShadowDir = GetShadowDirection( shadowHandle );</source>


At approximately line 3387, replace the line:
At approximately line 3387, replace the line:
<source lang="cpp">Vector vecShadowDir = GetShadowDirection( pSourceRenderable ); </source>
<source lang="cpp">Vector vecShadowDir = GetShadowDirection( pSourceRenderable );</source>
With:
With:
<source lang="cpp">Vector vecShadowDir = GetShadowDirection( handle );</source>
<source lang="cpp">Vector vecShadowDir = GetShadowDirection( handle );</source>


At approximately line 4222, below:
At approximately line 4222, below:
<source lang="cpp"> while( pChild )
<source lang="cpp">
while( pChild )
{
{
if( pChild->GetClientRenderable()==pRenderable )
if( pChild->GetClientRenderable()==pRenderable )
Line 500: Line 517:
}</source>
}</source>
Add:
Add:
<source lang="cpp">const Vector &CClientShadowMgr::GetShadowDirection( ClientShadowHandle_t shadowHandle ) const
<source lang="cpp">
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
const Vector &CClientShadowMgr::GetShadowDirection( ClientShadowHandle_t shadowHandle ) const
{
{
Assert( shadowHandle != CLIENTSHADOW_INVALID_HANDLE );
Assert( shadowHandle != CLIENTSHADOW_INVALID_HANDLE );
 
IClientRenderable* pRenderable = ClientEntityList().GetClientRenderableFromHandle( m_Shadows[shadowHandle].m_Entity );
IClientRenderable *pRenderable = ClientEntityList().GetClientRenderableFromHandle( m_Shadows[shadowHandle].m_Entity );
Assert( pRenderable );
Assert( pRenderable );
 
if ( !IsShadowingFromWorldLights() )
if ( !IsShadowingFromWorldLights() )
{
{
return GetShadowDirection( pRenderable );
return GetShadowDirection( pRenderable );
}
}
 
Vector &vecResult = AllocTempVector();
Vector &vecResult = AllocTempVector();
vecResult = m_Shadows[shadowHandle].m_ShadowDir;
vecResult = m_Shadows[shadowHandle].m_ShadowDir;
 
// Allow the renderable to override the default
// Allow the renderable to override the default
pRenderable->GetShadowCastDirection( &vecResult, GetActualShadowCastType( pRenderable ) );
pRenderable->GetShadowCastDirection( &vecResult, GetActualShadowCastType( pRenderable ) );
 
return vecResult;
return vecResult;
}
}
 
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CClientShadowMgr::UpdateShadowDirectionFromLocalLightSource( ClientShadowHandle_t shadowHandle )
void CClientShadowMgr::UpdateShadowDirectionFromLocalLightSource( ClientShadowHandle_t shadowHandle )
{
{
Assert( shadowHandle != CLIENTSHADOW_INVALID_HANDLE );
Assert( shadowHandle != CLIENTSHADOW_INVALID_HANDLE );
 
ClientShadow_t& shadow = m_Shadows[shadowHandle];
ClientShadow_t &shadow = m_Shadows[shadowHandle];
 
IClientRenderable* pRenderable = ClientEntityList().GetClientRenderableFromHandle( shadow.m_Entity );
IClientRenderable *pRenderable = ClientEntityList().GetClientRenderableFromHandle( shadow.m_Entity );
 
// TODO: Figure out why this still gets hit
// TODO: Figure out why this still gets hit
Assert( pRenderable );
Assert( pRenderable );
Line 536: Line 560:
return;
return;
}
}
 
Vector bbMin, bbMax;
Vector bbMin, bbMax;
pRenderable->GetRenderBoundsWorldspace( bbMin, bbMax );
pRenderable->GetRenderBoundsWorldspace( bbMin, bbMax );
Vector origin( 0.5f * ( 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
origin.z = bbMin.z; // Putting origin at the bottom of the bounding box makes the shadows a little shorter
 
Vector lightPos;
Vector lightPos;
Vector lightBrightness;
Vector lightBrightness;
 
if ( shadow.m_LightPosLerp >= 1.0f ) // skip finding new light source if we're in the middle of a lerp
if ( shadow.m_LightPosLerp >= 1.0f ) // Skip finding new light source if we're in the middle of a lerp
{
{
// Calculate minimum brightness squared
// Calculate minimum brightness squared
float flMinBrightnessSqr = r_worldlight_mincastintensity.GetFloat();
float flMinBrightnessSqr = r_worldlight_mincastintensity.GetFloat();
flMinBrightnessSqr *= flMinBrightnessSqr;
flMinBrightnessSqr *= flMinBrightnessSqr;
 
if(g_pWorldLights->GetBrightestLightSource(pRenderable->GetRenderOrigin(), lightPos, lightBrightness) == false ||
if ( g_pWorldLights->GetBrightestLightSource( pRenderable->GetRenderOrigin(), lightPos, lightBrightness ) == false || lightBrightness.LengthSqr() < flMinBrightnessSqr )
lightBrightness.LengthSqr() < flMinBrightnessSqr )
{
{
// didn't find a light source at all, use default shadow direction
// Didn't find a light source at all, use default shadow direction
// TODO: Could switch to using blobby shadow in this case
// TODO: Could switch to using blobby shadow in this case
lightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
lightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
}
}
}
}
 
if ( shadow.m_LightPosLerp == FLT_MAX ) // first light pos ever, just init
if ( shadow.m_LightPosLerp == FLT_MAX ) // First light pos ever, just init
{
{
shadow.m_CurrentLightPos = lightPos;
shadow.m_CurrentLightPos = lightPos;
Line 569: Line 592:
{
{
// We're in the middle of a lerp from current to target light. Finish it.
// 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 += gpGlobals->frametime * 1.0f / r_worldlight_lerptime.GetFloat();
shadow.m_LightPosLerp = clamp( shadow.m_LightPosLerp, 0.0f, 1.0f );
shadow.m_LightPosLerp = clamp( shadow.m_LightPosLerp, 0.0f, 1.0f );
 
Vector currLightPos( shadow.m_CurrentLightPos );
Vector currLightPos( shadow.m_CurrentLightPos );
Vector targetLightPos( shadow.m_TargetLightPos );
Vector targetLightPos( shadow.m_TargetLightPos );
Line 582: Line 605:
targetLightPos = origin - 200.0f * GetShadowDirection();
targetLightPos = origin - 200.0f * GetShadowDirection();
}
}
 
// lerp light pos
// Lerp light pos
Vector v1 = origin - shadow.m_CurrentLightPos;
Vector v1 = origin - shadow.m_CurrentLightPos;
v1.NormalizeInPlace();
v1.NormalizeInPlace();
 
Vector v2 = origin - shadow.m_TargetLightPos;
Vector v2 = origin - shadow.m_TargetLightPos;
v2.NormalizeInPlace();
v2.NormalizeInPlace();
 
// SAULUNDONE: caused over top sweeping far too often
// SAULUNDONE: Caused over top sweeping far too often
#if 0
#if 0
if ( v1.Dot( v2 ) < 0.0f )
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
// 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 );
Vector fakeOverheadLightPos( origin.x, origin.y, origin.z + 200.0f );
if( shadow.m_LightPosLerp < 0.5f )
if ( shadow.m_LightPosLerp < 0.5f )
{
{
lightPos = Lerp( 2.0f * shadow.m_LightPosLerp, currLightPos, fakeOverheadLightPos );
lightPos = Lerp( 2.0f * shadow.m_LightPosLerp, currLightPos, fakeOverheadLightPos );
Line 610: Line 633:
lightPos = Lerp( shadow.m_LightPosLerp, currLightPos, targetLightPos );
lightPos = Lerp( shadow.m_LightPosLerp, currLightPos, targetLightPos );
}
}
 
if ( shadow.m_LightPosLerp >= 1.0f )
if ( shadow.m_LightPosLerp >= 1.0f )
{
{
Line 618: Line 641:
else if ( shadow.m_LightPosLerp >= 1.0f )
else if ( shadow.m_LightPosLerp >= 1.0f )
{
{
// check if we have a new closest light position and start a new lerp
// Check if we have a new closest light position and start a new lerp
float flDistSq = ( lightPos - shadow.m_CurrentLightPos ).LengthSqr();
float flDistSq = ( lightPos - shadow.m_CurrentLightPos ).LengthSqr();
 
if ( flDistSq > 1.0f )
if ( flDistSq > 1.0f )
{
{
// light position has changed, which means we got a new light source. Initiate a lerp
// Light position has changed, which means we got a new light source. Initiate a lerp
shadow.m_TargetLightPos = lightPos;
shadow.m_TargetLightPos = lightPos;
shadow.m_LightPosLerp = 0.0f;
shadow.m_LightPosLerp = 0.0f;
}
}
 
lightPos = shadow.m_CurrentLightPos;
lightPos = shadow.m_CurrentLightPos;
}
}
 
if ( lightPos.x == FLT_MAX )
if ( lightPos.x == FLT_MAX )
{
{
lightPos = origin - 200.0f * GetShadowDirection();
lightPos = origin - 200.0f * GetShadowDirection();
}
}
 
Vector vecResult( origin - lightPos );
Vector vecResult( origin - lightPos );
vecResult.NormalizeInPlace();
vecResult.NormalizeInPlace();
 
vecResult.z *= r_worldlight_shortenfactor.GetFloat();
vecResult.z *= r_worldlight_shortenfactor.GetFloat();
vecResult.NormalizeInPlace();
vecResult.NormalizeInPlace();
 
shadow.m_ShadowDir = vecResult;
shadow.m_ShadowDir = vecResult;
 
if ( r_worldlight_debug.GetBool() )
if ( r_worldlight_debug.GetBool() )
{
{
Line 649: Line 672:
}
}
}
}
 
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CClientShadowMgr::UpdateDirtyShadow( ClientShadowHandle_t handle )
void CClientShadowMgr::UpdateDirtyShadow( ClientShadowHandle_t handle )
{
{
Assert( m_Shadows.IsValidIndex( handle ) );
Assert( m_Shadows.IsValidIndex( handle ) );
   
   
if( IsShadowingFromWorldLights() )
if ( IsShadowingFromWorldLights() )
UpdateShadowDirectionFromLocalLightSource( handle );
UpdateShadowDirectionFromLocalLightSource( handle );
   
   
UpdateProjectedTextureInternal( handle, false );
UpdateProjectedTextureInternal( handle, false );
}
}
 
void WorldLightCastShadowCallback(IConVar *pVar, const char *pszOldValue, float flOldValue)
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void WorldLightCastShadowCallback( IConVar *pVar, const char *pszOldValue, float flOldValue )
{
{
s_ClientShadowMgr.SetShadowFromWorldLightsEnabled(r_worldlight_castshadows.GetBool());
s_ClientShadowMgr.SetShadowFromWorldLightsEnabled( r_worldlight_castshadows.GetBool() );
}
}
 
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CClientShadowMgr::SetShadowFromWorldLightsEnabled( bool bEnabled )
void CClientShadowMgr::SetShadowFromWorldLightsEnabled( bool bEnabled )
{
{
if(bEnabled == IsShadowingFromWorldLights())
if ( bEnabled == IsShadowingFromWorldLights() )
return;
return;
   
   
m_bShadowFromWorldLights = bEnabled;
m_bShadowFromWorldLights = bEnabled;
UpdateAllShadows();
UpdateAllShadows();
}</source>
}
 
</source>


==Complete source code of clientshadowmgr.cpp==
==Complete source code of clientshadowmgr.cpp==
Line 688: Line 720:
Happy modding!
Happy modding!


[[Category:Lighting]]
[[Category:Tutorials]]
[[Category:Tutorials]]
[[Category:Programming]]
[[Category:Programming]]

Latest revision as of 04:17, 30 May 2025

English (en)Translate (Translate)
The difference between a mod with this code(shadows facing away from light) and a mod/game without it(shadows directly under entities).

Introduction

This article explains how to implement dynamic render-to-texture shadow angles in both Source 2007, Source 2013 and Team Fortress 2 branch 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 Left 4 Dead, Portal 2 Portal 2, Alien Swarm Alien Swarm, Dark Messiah of Might and Magic Dark Messiah of Might and Magic and Mapbase 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, Source 2013 as well as Team Fortress 2 branch.

Icon-Bug.pngBug:This dynamic shadow angle implementation doesn't work correctly, the shadows have lerping issues when standing in between 2 lights.

game/client/worldlight.cpp

Create a new file in your /game/client folder called worldlight.cpp, and paste the following code into it.

Warning.pngWarning: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.
//========= 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:

Note.pngNote:Don’t forget to add these files to your client project
//========= 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!