Dynamic RTT shadow angles in Source 2007: Difference between revisions
No edit summary |
|||
(21 intermediate revisions by 7 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== | ==Introduction== | ||
This article explains how to implement dynamic [[Dynamic Shadows|render-to-texture]] shadow angles in both | 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. | ||
In | 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}}. | ||
{{bug|This dynamic shadow angle | {{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| | {{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) | //========= Copyright (C) 2021, CSProMod Team, All rights reserved. =========// | ||
// | // | ||
// Purpose: | // 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 | // 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 | // 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 35: | Line 35: | ||
#include "cbase.h" | #include "cbase.h" | ||
#include "bspfile.h" | #include "bspfile.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" | |||
// memdbgon must be the last include file in a .cpp file!!! | |||
#include "tier0/memdbgon.h" | |||
static IVEngineServer *g_pEngineServer = nullptr; | |||
static | |||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: | // Purpose: Calculate intensity ratio for a worldlight by distance | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
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 ); | ||
case emit_skylight: | case emit_skylight: | ||
return 1. | return 1.0f; | ||
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. | return 0.0f; | ||
return falloff; | return falloff; | ||
case emit_skyambient: | case emit_skyambient: | ||
return 1. | return 1.0f; | ||
case emit_point: | case emit_point: | ||
case emit_spotlight: // | 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. | return 0.0f; | ||
return 1. | return 1.0f / ( wl->constant_attn + wl->linear_attn * dist + wl->quadratic_attn * dist2 ); | ||
} | } | ||
} | } | ||
return 1. | return 1.0f; | ||
} | } | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: | // Purpose: Initialize game system and members | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
CWorldLights::CWorldLights() : CAutoGameSystem("World lights") | CWorldLights::CWorldLights() : CAutoGameSystem( "World lights" ) | ||
{ | { | ||
m_nWorldLights = 0; | m_nWorldLights = 0; | ||
m_pWorldLights = | m_pWorldLights = nullptr; | ||
} | } | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: | // Purpose: Clear worldlights, free memory | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
void CWorldLights::Clear() | void CWorldLights::Clear() | ||
Line 124: | Line 114: | ||
m_nWorldLights = 0; | m_nWorldLights = 0; | ||
if(m_pWorldLights) | if ( m_pWorldLights ) | ||
{ | { | ||
delete [] m_pWorldLights; | delete[] m_pWorldLights; | ||
m_pWorldLights = | m_pWorldLights = nullptr; | ||
} | } | ||
} | } | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: | // 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 = | if ( ( g_pEngineServer = static_cast<IVEngineServer *>( factories.appSystemFactory( INTERFACEVERSION_VENGINESERVER, nullptr ) ) ) == nullptr ) | ||
return false; | return false; | ||
Line 146: | Line 136: | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: | // 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: | Warning( "CWorldLights: Unable to open map\n" ); | ||
return; | return; | ||
} | } | ||
Line 164: | 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: | 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: | DevMsg( "CWorldLights: Load successful (%d lights at 0x%p)\n", m_nWorldLights, m_pWorldLights ); | ||
} | } | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: | // 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 206: | 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, | 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, | 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 244: | 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 265: | 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 291: | 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, | 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 303: | 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 316: | 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| | {{Note|Don’t forget to add these files to your client project}} | ||
<source lang="cpp"> | <source lang="cpp"> | ||
//========= Copyright (C) | //========= Copyright (C) 2021, CSProMod Team, All rights reserved. =========// | ||
// | |||
// Purpose: Provide world light-related functions to the client | |||
// | // | ||
// Written: November 2011 | // Written: November 2011 | ||
// Author: Saul Rennison | // Author: Saul Rennison | ||
Line 328: | Line 332: | ||
//===========================================================================// | //===========================================================================// | ||
#ifndef WORLDLIGHT_H | |||
#define WORLDLIGHT_H | |||
#ifdef _WIN32 | |||
#pragma once | #pragma once | ||
#endif | |||
#include "igamesystem.h" // CAutoGameSystem | #include "igamesystem.h" // CAutoGameSystem | ||
Line 344: | Line 352: | ||
~CWorldLights() { Clear(); } | ~CWorldLights() { Clear(); } | ||
bool GetBrightestLightSource( const Vector &vecPosition, Vector &vecLightPos, Vector &vecLightBrightness ); | |||
bool GetBrightestLightSource(const Vector &vecPosition, Vector &vecLightPos, Vector &vecLightBrightness); | |||
// CAutoGameSystem overrides | // CAutoGameSystem overrides | ||
bool Init() OVERRIDE; | |||
void LevelInitPreEntity() OVERRIDE; | |||
void LevelShutdownPostEntity() OVERRIDE { 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 | |||
extern CWorldLights *g_pWorldLights; | extern CWorldLights *g_pWorldLights; | ||
#endif // WORLDLIGHT_H | |||
</source> | </source> | ||
Line 383: | 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 396: | 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 409: | 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 449: | 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 465: | 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 502: | 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 538: | 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 ) // | 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 ) | ||
{ | { | ||
// | // 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 ) // | if ( shadow.m_LightPosLerp == FLT_MAX ) // First light pos ever, just init | ||
{ | { | ||
shadow.m_CurrentLightPos = lightPos; | shadow.m_CurrentLightPos = lightPos; | ||
Line 571: | 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 584: | Line 605: | ||
targetLightPos = origin - 200.0f * GetShadowDirection(); | targetLightPos = origin - 200.0f * GetShadowDirection(); | ||
} | } | ||
// | // 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: | // 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 | ||
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 612: | 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 620: | 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 | ||
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 | ||
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 651: | 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 690: | 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


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,
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, Source 2013 as well as Team Fortress 2 branch.

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!