2019-08-31 22:28:20 +03:00
//========= 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 ;
2021-04-24 06:58:16 +03:00
# ifdef MAPBASE
ConVar cl_worldlight_use_new_method ( " cl_worldlight_use_new_method " , " 1 " , FCVAR_NONE , " Uses the new world light iteration method which splits lights into multiple lists for each cluster. " ) ;
# endif
2019-08-31 22:28:20 +03:00
//-----------------------------------------------------------------------------
// 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 ( )
{
2019-09-08 00:05:59 +03:00
# ifdef MAPBASE
// Moved to its own clientside interface after I found out it was possible
// (still have no idea how or why this works)
if ( ( g_pEngineServer = serverengine ) = = NULL )
return false ;
return true ;
# else
2019-08-31 22:28:20 +03:00
factorylist_t factories ;
FactoryList_Retrieve ( factories ) ;
if ( ( g_pEngineServer = ( IVEngineServer * ) factories . appSystemFactory ( INTERFACEVERSION_VENGINESERVER , NULL ) ) = = NULL )
return false ;
return true ;
2019-09-08 00:05:59 +03:00
# endif
2019-08-31 22:28:20 +03:00
}
//-----------------------------------------------------------------------------
// 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 ] ;
// INSOLENCE: 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 ) ;
2021-04-24 06:58:16 +03:00
# ifdef MAPBASE
// Now that the lights have been gathered, begin separating them into lists for each PVS cluster.
// This code is adapted from the soundscape cluster list code (see soundscape_system.cpp) and is intended to
// reduce frame drops in large maps which use dynamic RTT shadow angles.
CUtlVector < bbox_t > clusterbounds ;
int clusterCount = g_pEngineServer - > GetClusterCount ( ) ;
clusterbounds . SetCount ( clusterCount ) ;
g_pEngineServer - > GetAllClusterBounds ( clusterbounds . Base ( ) , clusterCount ) ;
m_WorldLightsInCluster . SetCount ( clusterCount ) ;
for ( int i = 0 ; i < clusterCount ; i + + )
{
m_WorldLightsInCluster [ i ] . lightCount = 0 ;
m_WorldLightsInCluster [ i ] . firstLight = 0 ;
}
unsigned char myPVS [ 16 * 1024 ] ;
CUtlVector < short > clusterIndexList ;
CUtlVector < short > lightIndexList ;
// Find the clusters visible from each light, then add it to those clusters' light lists
// (Also try to clip for radius if possible)
for ( int i = 0 ; i < m_nWorldLights ; + + i )
{
dworldlight_t * light = & m_pWorldLights [ i ] ;
// Assign the sun to its own pointer
if ( light - > type = = emit_skylight )
{
m_iSunIndex = i ;
continue ;
}
float radiusSq = light - > radius * light - > radius ;
if ( radiusSq = = 0.0f )
{
// TODO: Use intensity instead?
radiusSq = FLT_MAX ;
}
g_pEngineServer - > GetPVSForCluster ( light - > cluster , sizeof ( myPVS ) , myPVS ) ;
for ( int j = 0 ; j < clusterCount ; j + + )
{
if ( myPVS [ j > > 3 ] & ( 1 < < ( j & 7 ) ) )
{
float distSq = CalcSqrDistanceToAABB ( clusterbounds [ j ] . mins , clusterbounds [ j ] . maxs , light - > origin ) ;
if ( distSq < radiusSq )
{
m_WorldLightsInCluster [ j ] . lightCount + + ;
clusterIndexList . AddToTail ( j ) ;
lightIndexList . AddToTail ( i ) ;
}
}
}
}
m_WorldLightsIndexList . SetCount ( lightIndexList . Count ( ) ) ;
// Compute the starting index of each cluster
int firstLight = 0 ;
for ( int i = 0 ; i < clusterCount ; i + + )
{
m_WorldLightsInCluster [ i ] . firstLight = firstLight ;
firstLight + = m_WorldLightsInCluster [ i ] . lightCount ;
m_WorldLightsInCluster [ i ] . lightCount = 0 ;
}
// Now add each light index to the appropriate cluster's list
for ( int i = 0 ; i < lightIndexList . Count ( ) ; i + + )
{
int cluster = clusterIndexList [ i ] ;
int outIndex = m_WorldLightsInCluster [ cluster ] . lightCount + m_WorldLightsInCluster [ cluster ] . firstLight ;
m_WorldLightsInCluster [ cluster ] . lightCount + + ;
m_WorldLightsIndexList [ outIndex ] = lightIndexList [ i ] ;
}
//DevMsg( "CWorldLights: Light clusters list has %i elements; Light index list has %i\n", m_WorldLightsInCluster.Count(), m_WorldLightsIndexList.Count() );
# endif
2019-08-31 22:28:20 +03:00
}
//-----------------------------------------------------------------------------
// 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 ) ;
2021-04-24 06:58:16 +03:00
# ifdef MAPBASE
if ( cl_worldlight_use_new_method . GetBool ( ) )
{
FindBrightestLightSourceNew ( vecPosition , vecLightPos , vecLightBrightness , nCluster ) ;
}
else
# endif
{
FindBrightestLightSourceOld ( vecPosition , vecLightPos , vecLightBrightness , nCluster ) ;
}
//engine->Con_NPrintf(m_nWorldLights, "result: %d", !vecLightBrightness.IsZero());
return ! vecLightBrightness . IsZero ( ) ;
}
void CWorldLights : : FindBrightestLightSourceOld ( const Vector & vecPosition , Vector & vecLightPos , Vector & vecLightBrightness , int nCluster )
{
// Find the size of the PVS for our current position
2019-08-31 22:28:20 +03:00
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 ;
2021-04-24 06:58:16 +03:00
return ;
2019-08-31 22:28:20 +03:00
}
// 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 ;
2021-04-24 06:58:16 +03:00
}
2019-08-31 22:28:20 +03:00
2021-04-24 06:58:16 +03:00
# ifdef MAPBASE
void CWorldLights : : FindBrightestLightSourceNew ( const Vector & vecPosition , Vector & vecLightPos , Vector & vecLightBrightness , int nCluster )
{
// Handle sun
if ( m_iSunIndex ! = - 1 )
{
dworldlight_t * light = & m_pWorldLights [ m_iSunIndex ] ;
// 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 ( ) )
{
// 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 ) )
{
// Act like we didn't find any valid worldlights, so the shadow
// manager uses the default shadow direction instead (should be the
// sun direction)
return ;
}
}
}
// Iterate through all the worldlights
if ( nCluster > = 0 & & nCluster < m_WorldLightsInCluster . Count ( ) )
{
// find all soundscapes that could possibly attach to this player and update them
for ( int j = 0 ; j < m_WorldLightsInCluster [ nCluster ] . lightCount ; j + + )
{
int ssIndex = m_WorldLightsIndexList [ m_WorldLightsInCluster [ nCluster ] . firstLight + j ] ;
dworldlight_t * light = & m_pWorldLights [ ssIndex ] ;
// 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 ;
}
// 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());
}
}
}
# endif