Dynamic RTT shadow angles in Source 2007

From Valve Developer Community
Jump to: navigation, search
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 and Source 2013 mods. Render-to-texture shadows are cast by most non-static renderables (dynamic props, players, NPCs, etc.) in the Source Engine. The direction of the shadow they cast is generally given by the shadow_control entity, but it affects all entities in a level.

In Left 4 Dead, Portal 2 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 aswell as Source 2013.

game/client/worldlight.cpp

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

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.

//========= Copyright (C) 2018, 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 initialised 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 which 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 "worldlight.h"
#include "bspfile.h"
#include "filesystem.h"
#include "client_factorylist.h" // FactoryList_Retrieve
#include "eiface.h" // IVEngineServer

static IVEngineServer *g_pEngineServer = NULL;

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

//-----------------------------------------------------------------------------
// Purpose: calculate intensity ratio for a worldlight by distance
// Author: Valve Software
//-----------------------------------------------------------------------------
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);
		break;

	case emit_skylight:
		return 1.f;
		break;

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

		return falloff;
		break;

	case emit_skyambient:
		return 1.f;
		break;

	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.f;

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

		break;
	}

	return 1.f;
}

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

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

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

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

	if((g_pEngineServer = (IVEngineServer*)factories.appSystemFactory(INTERFACEVERSION_VENGINESERVER, NULL)) == NULL)
		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 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, NULL);

	// 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, NULL, 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, NULL, 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();
}

game/client/worldlight.h

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

Note:don’t forget to add these files to your client project
//========= Copyright (C) 2018, CSProMod Team, All rights reserved. =========//
//
// Purpose: provide world light related functions to the client
// 
// Written: November 2011
// Author: Saul Rennison
//
//===========================================================================//

#pragma once

#include "igamesystem.h" // CAutoGameSystem

class Vector;
struct dworldlight_t;

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
class CWorldLights : public CAutoGameSystem
{
public:
	CWorldLights();
	~CWorldLights() { Clear(); }

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

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

private:
	void Clear();

	int m_nWorldLights;
	dworldlight_t *m_pWorldLights;
};

//-----------------------------------------------------------------------------
// Singleton exposure
//-----------------------------------------------------------------------------
extern CWorldLights *g_pWorldLights;

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:

const Vector &CClientShadowMgr::GetShadowDirection( ClientShadowHandle_t shadowHandle ) const
{
	Assert( shadowHandle != CLIENTSHADOW_INVALID_HANDLE );
 
	IClientRenderable* pRenderable = ClientEntityList().GetClientRenderableFromHandle( m_Shadows[shadowHandle].m_Entity );
	Assert( pRenderable );
 
	if ( !IsShadowingFromWorldLights() )
	{
		return GetShadowDirection( pRenderable );
	}
 
	Vector &vecResult = AllocTempVector();
	vecResult = m_Shadows[shadowHandle].m_ShadowDir;
 
	// Allow the renderable to override the default
	pRenderable->GetShadowCastDirection( &vecResult, GetActualShadowCastType( pRenderable ) );
 
	return vecResult;
}
 
void CClientShadowMgr::UpdateShadowDirectionFromLocalLightSource( ClientShadowHandle_t shadowHandle )
{
	Assert( shadowHandle != CLIENTSHADOW_INVALID_HANDLE );
 
	ClientShadow_t& shadow = m_Shadows[shadowHandle];
 
	IClientRenderable* pRenderable = ClientEntityList().GetClientRenderableFromHandle( shadow.m_Entity );
 
	// TODO: Figure out why this still gets hit
	Assert( pRenderable );
	if ( !pRenderable )
	{
		DevWarning( "%s(): Skipping shadow with invalid client renderable (shadow handle %d)\n", __FUNCTION__, shadowHandle );
		return;
	}
 
	Vector bbMin, bbMax;
	pRenderable->GetRenderBoundsWorldspace( bbMin, bbMax );
	Vector origin( 0.5f * ( bbMin + bbMax ) );
	origin.z = bbMin.z;	// Putting origin at the bottom of the bounding box makes the shadows a little shorter
 
	Vector lightPos;
	Vector lightBrightness;
 
	if ( shadow.m_LightPosLerp >= 1.0f )	// skip finding new light source if we're in the middle of a lerp
	{
		// Calculate minimum brightness squared
		float flMinBrightnessSqr = r_worldlight_mincastintensity.GetFloat();
		flMinBrightnessSqr *= flMinBrightnessSqr;
 
		if(g_pWorldLights->GetBrightestLightSource(pRenderable->GetRenderOrigin(), lightPos, lightBrightness) == false ||
			lightBrightness.LengthSqr() < flMinBrightnessSqr )
		{
			// didn't find a light source at all, use default shadow direction
			// TODO: Could switch to using blobby shadow in this case
			lightPos.Init( FLT_MAX, FLT_MAX, FLT_MAX );
		}
	}
 
	if ( shadow.m_LightPosLerp == FLT_MAX )	// first light pos ever, just init
	{
		shadow.m_CurrentLightPos = lightPos;
		shadow.m_TargetLightPos = lightPos;
		shadow.m_LightPosLerp = 1.0f;
	}
	else if ( shadow.m_LightPosLerp < 1.0f )
	{
		// We're in the middle of a lerp from current to target light. Finish it.
		shadow.m_LightPosLerp += gpGlobals->frametime * 1.0f/r_worldlight_lerptime.GetFloat();
		shadow.m_LightPosLerp = clamp( shadow.m_LightPosLerp, 0.0f, 1.0f );
 
		Vector currLightPos( shadow.m_CurrentLightPos );
		Vector targetLightPos( shadow.m_TargetLightPos );
		if ( currLightPos.x == FLT_MAX )
		{
			currLightPos = origin - 200.0f * GetShadowDirection();
		}
		if ( targetLightPos.x == FLT_MAX )
		{
			targetLightPos = origin - 200.0f * GetShadowDirection();
		}
 
		// lerp light pos
		Vector v1 = origin - shadow.m_CurrentLightPos;
		v1.NormalizeInPlace();
 
		Vector v2 = origin - shadow.m_TargetLightPos;
		v2.NormalizeInPlace();
 
		// SAULUNDONE: caused over top sweeping far too often
#if 0
		if ( v1.Dot( v2 ) < 0.0f )
		{
			// if change in shadow angle is more than 90 degrees, lerp over the renderable's top to avoid long sweeping shadows
			Vector fakeOverheadLightPos( origin.x, origin.y, origin.z + 200.0f );
			if( shadow.m_LightPosLerp < 0.5f )
			{
				lightPos = Lerp( 2.0f * shadow.m_LightPosLerp, currLightPos, fakeOverheadLightPos );
			}
			else
			{
				lightPos = Lerp( 2.0f * shadow.m_LightPosLerp - 1.0f, fakeOverheadLightPos, targetLightPos );
			}
		}
		else
#endif
		{
			lightPos = Lerp( shadow.m_LightPosLerp, currLightPos, targetLightPos );
		}
 
		if ( shadow.m_LightPosLerp >= 1.0f )
		{
			shadow.m_CurrentLightPos = shadow.m_TargetLightPos;
		}
	}
	else if ( shadow.m_LightPosLerp >= 1.0f )
	{
		// check if we have a new closest light position and start a new lerp
		float flDistSq = ( lightPos - shadow.m_CurrentLightPos ).LengthSqr();
 
		if ( flDistSq > 1.0f )
		{
			// light position has changed, which means we got a new light source. Initiate a lerp
			shadow.m_TargetLightPos = lightPos;
			shadow.m_LightPosLerp = 0.0f;
		}
 
		lightPos = shadow.m_CurrentLightPos;
	}
 
	if ( lightPos.x == FLT_MAX )
	{
		lightPos = origin - 200.0f * GetShadowDirection();
	}
 
	Vector vecResult( origin - lightPos );
	vecResult.NormalizeInPlace();
 
	vecResult.z *= r_worldlight_shortenfactor.GetFloat();
	vecResult.NormalizeInPlace();
 
	shadow.m_ShadowDir = vecResult;
 
	if ( r_worldlight_debug.GetBool() )
	{
		NDebugOverlay::Line( lightPos, origin, 255, 255, 0, false, 0.0f );
	}
}
 
void CClientShadowMgr::UpdateDirtyShadow( ClientShadowHandle_t handle )
{
	Assert( m_Shadows.IsValidIndex( handle ) );
 
	if( IsShadowingFromWorldLights() )
		UpdateShadowDirectionFromLocalLightSource( handle );
 
	UpdateProjectedTextureInternal( handle, false );
}
 
void WorldLightCastShadowCallback(IConVar *pVar, const char *pszOldValue, float flOldValue)
{
	s_ClientShadowMgr.SetShadowFromWorldLightsEnabled(r_worldlight_castshadows.GetBool());
}
 
void CClientShadowMgr::SetShadowFromWorldLightsEnabled( bool bEnabled )
{
	if(bEnabled == IsShadowingFromWorldLights())
		return;
 
	m_bShadowFromWorldLights = bEnabled;
	UpdateAllShadows();
}


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!