//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "c_smoke_trail.h" #include "smoke_fog_overlay.h" #include "engine/IEngineTrace.h" #include "view.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define SF_EMISSIVE 0x00000001 // ------------------------------------------------------------------------- // // Definitions // ------------------------------------------------------------------------- // static Vector s_FadePlaneDirections[] = { Vector( 1,0,0), Vector(-1,0,0), Vector(0, 1,0), Vector(0,-1,0), Vector(0,0, 1), Vector(0,0,-1) }; #define NUM_FADE_PLANES (sizeof(s_FadePlaneDirections)/sizeof(s_FadePlaneDirections[0])) // ------------------------------------------------------------------------- // // Classes // ------------------------------------------------------------------------- // class C_FuncSmokeVolume : public C_BaseParticleEntity, public IPrototypeAppEffect { public: DECLARE_CLASS( C_FuncSmokeVolume, C_BaseParticleEntity ); DECLARE_CLIENTCLASS(); C_FuncSmokeVolume(); ~C_FuncSmokeVolume(); int IsEmissive( void ) { return ( m_spawnflags & SF_EMISSIVE ); } private: class SmokeGrenadeParticle : public Particle { public: float m_RotationFactor; float m_CurRotation; float m_FadeAlpha; // Set as it moves around. unsigned char m_ColorInterp; // Amount between min and max colors. unsigned char m_Color[4]; }; // C_BaseEntity. public: virtual void OnDataChanged( DataUpdateType_t updateType ); // IPrototypeAppEffect. public: virtual void Start( CParticleMgr *pParticleMgr, IPrototypeArgAccess *pArgs ); // IParticleEffect. public: virtual void Update(float fTimeDelta); virtual void RenderParticles( CParticleRenderIterator *pIterator ); virtual void SimulateParticles( CParticleSimulateIterator *pIterator ); virtual void NotifyRemove(); private: // The SmokeEmitter represents a grid in 3D space. class SmokeParticleInfo { public: SmokeGrenadeParticle *m_pParticle; int m_TradeIndex; // -1 if not exchanging yet. float m_TradeClock; // How long since they started trading. float m_TradeDuration; // How long the trade will take to finish. float m_FadeAlpha; // Calculated from nearby world geometry. unsigned char m_Color[4]; }; inline int GetSmokeParticleIndex(int x, int y, int z) { Assert( IsValidXYZCoords( x, y, z ) ); return z*m_xCount*m_yCount+y*m_xCount+x; } inline SmokeParticleInfo *GetSmokeParticleInfo(int x, int y, int z) { Assert( IsValidXYZCoords( x, y, z ) ); return &m_pSmokeParticleInfos[GetSmokeParticleIndex(x,y,z)]; } inline void GetParticleInfoXYZ(int index, int &x, int &y, int &z) { Assert( index >= 0 && index < m_xCount * m_yCount * m_zCount ); z = index / (m_xCount*m_yCount); int zIndex = z*m_xCount*m_yCount; y = (index - zIndex) / m_xCount; int yIndex = y*m_xCount; x = index - zIndex - yIndex; Assert( IsValidXYZCoords( x, y, z ) ); } inline bool IsValidXYZCoords(int x, int y, int z) { return x >= 0 && y >= 0 && z >= 0 && x < m_xCount && y < m_yCount && z < m_zCount; } inline Vector GetSmokeParticlePos(int x, int y, int z ) { return WorldAlignMins() + Vector( x * m_SpacingRadius * 2 + m_SpacingRadius, y * m_SpacingRadius * 2 + m_SpacingRadius, z * m_SpacingRadius * 2 + m_SpacingRadius ); } inline Vector GetSmokeParticlePosIndex(int index) { int x, y, z; GetParticleInfoXYZ(index, x, y, z); return GetSmokeParticlePos(x, y, z); } // Start filling the smoke volume void FillVolume(); private: // State variables from server. color32 m_Color1; color32 m_Color2; char m_MaterialName[255]; float m_ParticleDrawWidth; float m_ParticleSpacingDistance; float m_DensityRampSpeed; float m_RotationSpeed; float m_MovementSpeed; float m_Density; float m_maxDrawDistance; int m_spawnflags; private: C_FuncSmokeVolume( const C_FuncSmokeVolume & ); float m_CurrentDensity; float m_ParticleRadius; bool m_bStarted; PMaterialHandle m_MaterialHandle; SmokeParticleInfo *m_pSmokeParticleInfos; int m_xCount, m_yCount, m_zCount; float m_SpacingRadius; Vector m_MinColor; Vector m_MaxColor; Vector m_vLastOrigin; QAngle m_vLastAngles; bool m_bFirstUpdate; }; IMPLEMENT_CLIENTCLASS_DT( C_FuncSmokeVolume, DT_FuncSmokeVolume, CFuncSmokeVolume ) RecvPropInt( RECVINFO( m_Color1 ), 0, RecvProxy_Int32ToColor32 ), RecvPropInt( RECVINFO( m_Color2 ), 0, RecvProxy_Int32ToColor32 ), RecvPropString( RECVINFO( m_MaterialName ) ), RecvPropFloat( RECVINFO( m_ParticleDrawWidth ) ), RecvPropFloat( RECVINFO( m_ParticleSpacingDistance ) ), RecvPropFloat( RECVINFO( m_DensityRampSpeed ) ), RecvPropFloat( RECVINFO( m_RotationSpeed ) ), RecvPropFloat( RECVINFO( m_MovementSpeed ) ), RecvPropFloat( RECVINFO( m_Density ) ), RecvPropFloat( RECVINFO( m_maxDrawDistance ) ), RecvPropInt( RECVINFO( m_spawnflags ) ), RecvPropDataTable( RECVINFO_DT( m_Collision ), 0, &REFERENCE_RECV_TABLE(DT_CollisionProperty) ), END_RECV_TABLE() // Helpers. // ------------------------------------------------------------------------- // static inline void InterpColor(unsigned char dest[4], unsigned char src1[4], unsigned char src2[4], float percent) { dest[0] = (unsigned char)(src1[0] + (src2[0] - src1[0]) * percent); dest[1] = (unsigned char)(src1[1] + (src2[1] - src1[1]) * percent); dest[2] = (unsigned char)(src1[2] + (src2[2] - src1[2]) * percent); } static inline int GetWorldPointContents(const Vector &vPos) { #if defined(PARTICLEPROTOTYPE_APP) return 0; #else return enginetrace->GetPointContents( vPos ); #endif } static inline void WorldTraceLine( const Vector &start, const Vector &end, int contentsMask, trace_t *trace ) { #if defined(PARTICLEPROTOTYPE_APP) trace->fraction = 1; #else UTIL_TraceLine(start, end, contentsMask, NULL, COLLISION_GROUP_NONE, trace); #endif } static inline Vector EngineGetLightForPoint(const Vector &vPos) { #if defined(PARTICLEPROTOTYPE_APP) return Vector(1,1,1); #else return engine->GetLightForPoint(vPos, true); #endif } static inline Vector& EngineGetVecRenderOrigin() { #if defined(PARTICLEPROTOTYPE_APP) static Vector dummy(0,0,0); return dummy; #else extern Vector g_vecRenderOrigin[ MAX_SPLITSCREEN_PLAYERS ]; return g_vecRenderOrigin[ GET_ACTIVE_SPLITSCREEN_SLOT() ]; #endif } static inline float& EngineGetSmokeFogOverlayAlpha() { #if defined(PARTICLEPROTOTYPE_APP) static float dummy; return dummy; #else return g_SmokeFogOverlayAlpha; #endif } static inline C_BaseEntity* ParticleGetEntity( int index ) { #if defined(PARTICLEPROTOTYPE_APP) return NULL; #else return cl_entitylist->GetEnt( index ); #endif } // ------------------------------------------------------------------------- // // C_FuncSmokeVolume // ------------------------------------------------------------------------- // C_FuncSmokeVolume::C_FuncSmokeVolume() { m_bFirstUpdate = true; m_vLastOrigin.Init(); m_vLastAngles.Init(); m_pSmokeParticleInfos = NULL; m_SpacingRadius = 0.0f;; m_ParticleRadius = 0.0f; m_MinColor.Init( 1.0, 1.0, 1.0 ); m_MaxColor.Init( 1.0, 1.0, 1.0 ); } C_FuncSmokeVolume::~C_FuncSmokeVolume() { delete [] m_pSmokeParticleInfos; } void C_FuncSmokeVolume::OnDataChanged( DataUpdateType_t updateType ) { m_MinColor[0] = ( 1.0f / 255.0f ) * m_Color1.r; m_MinColor[1] = ( 1.0f / 255.0f ) * m_Color1.g; m_MinColor[2] = ( 1.0f / 255.0f ) * m_Color1.b; m_MaxColor[0] = ( 1.0f / 255.0f ) * m_Color2.r; m_MaxColor[1] = ( 1.0f / 255.0f ) * m_Color2.g; m_MaxColor[2] = ( 1.0f / 255.0f ) * m_Color2.b; m_ParticleRadius = m_ParticleDrawWidth * 0.5f; m_SpacingRadius = m_ParticleSpacingDistance * 0.5f; m_ParticleEffect.SetParticleCullRadius( m_ParticleRadius ); // Warning( "m_Density: %f\n", m_Density ); // Warning( "m_MovementSpeed: %f\n", m_MovementSpeed ); if(updateType == DATA_UPDATE_CREATED) { Vector size = WorldAlignMaxs() - WorldAlignMins(); m_xCount = 0.5f + ( size.x / ( m_SpacingRadius * 2.0f ) ); m_yCount = 0.5f + ( size.y / ( m_SpacingRadius * 2.0f ) ); m_zCount = 0.5f + ( size.z / ( m_SpacingRadius * 2.0f ) ); m_CurrentDensity = m_Density; delete [] m_pSmokeParticleInfos; m_pSmokeParticleInfos = new SmokeParticleInfo[m_xCount * m_yCount * m_zCount]; Start( ParticleMgr(), NULL ); } BaseClass::OnDataChanged( updateType ); } void C_FuncSmokeVolume::Start( CParticleMgr *pParticleMgr, IPrototypeArgAccess *pArgs ) { if( !pParticleMgr->AddEffect( &m_ParticleEffect, this ) ) return; m_MaterialHandle = m_ParticleEffect.FindOrAddMaterial( m_MaterialName ); FillVolume(); m_bStarted = true; } void C_FuncSmokeVolume::Update( float fTimeDelta ) { // Update our world space bbox if we've moved at all. // We do this manually because sometimes people make HUGE bboxes, and if they're constantly changing because their // particles wander outside the current bounds sometimes, it'll be linking them into all the leaves repeatedly. const Vector &curOrigin = GetAbsOrigin(); const QAngle &curAngles = GetAbsAngles(); if ( !VectorsAreEqual( curOrigin, m_vLastOrigin, 0.1 ) || fabs( curAngles.x - m_vLastAngles.x ) > 0.1 || fabs( curAngles.y - m_vLastAngles.y ) > 0.1 || fabs( curAngles.z - m_vLastAngles.z ) > 0.1 || m_bFirstUpdate ) { m_bFirstUpdate = false; m_vLastAngles = curAngles; m_vLastOrigin = curOrigin; Vector vWorldMins, vWorldMaxs; CollisionProp()->WorldSpaceAABB( &vWorldMins, &vWorldMaxs ); vWorldMins -= Vector( m_ParticleRadius, m_ParticleRadius, m_ParticleRadius ); vWorldMaxs += Vector( m_ParticleRadius, m_ParticleRadius, m_ParticleRadius ); m_ParticleEffect.SetBBox( vWorldMins, vWorldMaxs ); } float minViewOrigingDistance = FLT_MAX; FOR_EACH_VALID_SPLITSCREEN_PLAYER( hh ) { float d = CollisionProp()->CalcDistanceFromPoint( MainViewOrigin(hh) ); if ( d < minViewOrigingDistance ) minViewOrigingDistance = d; } float targetDensity = m_Density; if ( m_maxDrawDistance > 0 && minViewOrigingDistance > m_maxDrawDistance ) { targetDensity = 0.0f; } // lerp m_CurrentDensity towards m_Density at a rate of m_DensityRampSpeed if( m_CurrentDensity < targetDensity ) { m_CurrentDensity += m_DensityRampSpeed * fTimeDelta; if( m_CurrentDensity > targetDensity ) { m_CurrentDensity = targetDensity; } } else if( m_CurrentDensity > targetDensity ) { m_CurrentDensity -= m_DensityRampSpeed * fTimeDelta; if( m_CurrentDensity < targetDensity ) { m_CurrentDensity = targetDensity; } } if( m_CurrentDensity == 0.0f ) { return; } // This is used to randomize the direction it chooses to move a particle in. int offsetLookup[3] = {-1,0,1}; float tradeDurationMax = m_ParticleSpacingDistance / ( m_MovementSpeed + 0.1f ); float tradeDurationMin = tradeDurationMax * 0.5f; if ( IS_NAN( tradeDurationMax ) || IS_NAN( tradeDurationMin ) ) return; // Warning( "tradeDuration: [%f,%f]\n", tradeDurationMin, tradeDurationMax ); // Update all the moving traders and establish new ones. int nTotal = m_xCount * m_yCount * m_zCount; for( int i=0; i < nTotal; i++ ) { SmokeParticleInfo *pInfo = &m_pSmokeParticleInfos[i]; if(!pInfo->m_pParticle) continue; if(pInfo->m_TradeIndex == -1) { pInfo->m_pParticle->m_FadeAlpha = pInfo->m_FadeAlpha; pInfo->m_pParticle->m_Color[0] = pInfo->m_Color[0]; pInfo->m_pParticle->m_Color[1] = pInfo->m_Color[1]; pInfo->m_pParticle->m_Color[2] = pInfo->m_Color[2]; // Is there an adjacent one that's not trading? int x, y, z; GetParticleInfoXYZ(i, x, y, z); int xCountOffset = rand(); int yCountOffset = rand(); int zCountOffset = rand(); bool bFound = false; for(int xCount=0; xCount < 3 && !bFound; xCount++) { for(int yCount=0; yCount < 3 && !bFound; yCount++) { for(int zCount=0; zCount < 3; zCount++) { int testX = x + offsetLookup[(xCount+xCountOffset) % 3]; int testY = y + offsetLookup[(yCount+yCountOffset) % 3]; int testZ = z + offsetLookup[(zCount+zCountOffset) % 3]; if(testX == x && testY == y && testZ == z) continue; if(IsValidXYZCoords(testX, testY, testZ)) { SmokeParticleInfo *pOther = GetSmokeParticleInfo(testX, testY, testZ); if(pOther->m_pParticle && pOther->m_TradeIndex == -1) { // Ok, this one is looking to trade also. pInfo->m_TradeIndex = GetSmokeParticleIndex(testX, testY, testZ); pOther->m_TradeIndex = i; pInfo->m_TradeClock = pOther->m_TradeClock = 0; pOther->m_TradeDuration = pInfo->m_TradeDuration = FRand( tradeDurationMin, tradeDurationMax ); bFound = true; break; } } } } } } else { SmokeParticleInfo *pOther = &m_pSmokeParticleInfos[pInfo->m_TradeIndex]; assert(pOther->m_TradeIndex == i); // This makes sure the trade only gets updated once per frame. if(pInfo < pOther) { // Increment the trade clock.. pInfo->m_TradeClock = (pOther->m_TradeClock += fTimeDelta); int x, y, z; GetParticleInfoXYZ(i, x, y, z); Vector myPos = GetSmokeParticlePos(x, y, z); int otherX, otherY, otherZ; GetParticleInfoXYZ(pInfo->m_TradeIndex, otherX, otherY, otherZ); Vector otherPos = GetSmokeParticlePos(otherX, otherY, otherZ); // Is the trade finished? if(pInfo->m_TradeClock >= pInfo->m_TradeDuration) { pInfo->m_TradeIndex = pOther->m_TradeIndex = -1; pInfo->m_pParticle->m_Pos = otherPos; pOther->m_pParticle->m_Pos = myPos; SmokeGrenadeParticle *temp = pInfo->m_pParticle; pInfo->m_pParticle = pOther->m_pParticle; pOther->m_pParticle = temp; } else { // Ok, move them closer. float percent = (float)cos(pInfo->m_TradeClock * 2 * 1.57079632f / pInfo->m_TradeDuration); percent = percent * 0.5 + 0.5; pInfo->m_pParticle->m_FadeAlpha = pInfo->m_FadeAlpha + (pOther->m_FadeAlpha - pInfo->m_FadeAlpha) * (1 - percent); pOther->m_pParticle->m_FadeAlpha = pInfo->m_FadeAlpha + (pOther->m_FadeAlpha - pInfo->m_FadeAlpha) * percent; InterpColor(pInfo->m_pParticle->m_Color, pInfo->m_Color, pOther->m_Color, 1-percent); InterpColor(pOther->m_pParticle->m_Color, pInfo->m_Color, pOther->m_Color, percent); pInfo->m_pParticle->m_Pos = myPos + (otherPos - myPos) * (1 - percent); pOther->m_pParticle->m_Pos = myPos + (otherPos - myPos) * percent; } } } } } void C_FuncSmokeVolume::RenderParticles( CParticleRenderIterator *pIterator ) { if ( m_CurrentDensity == 0 ) return; const SmokeGrenadeParticle *pParticle = (const SmokeGrenadeParticle*)pIterator->GetFirst(); while ( pParticle ) { Vector renderPos = pParticle->m_Pos; // Fade out globally. float alpha = m_CurrentDensity; // Apply the precalculated fade alpha from world geometry. alpha *= pParticle->m_FadeAlpha; // TODO: optimize this whole routine! Vector color = m_MinColor + (m_MaxColor - m_MinColor) * (pParticle->m_ColorInterp / 255.1f); if ( IsEmissive() ) { color.x += pParticle->m_Color[0] / 255.0f; color.y += pParticle->m_Color[1] / 255.0f; color.z += pParticle->m_Color[2] / 255.0f; color.x = clamp( color.x, 0.0f, 1.0f ); color.y = clamp( color.y, 0.0f, 1.0f ); color.z = clamp( color.z, 0.0f, 1.0f ); } else { color.x *= pParticle->m_Color[0] / 255.0f; color.y *= pParticle->m_Color[1] / 255.0f; color.z *= pParticle->m_Color[2] / 255.0f; } Vector tRenderPos; TransformParticle( ParticleMgr()->GetModelView(), renderPos, tRenderPos ); float sortKey = 1;//tRenderPos.z; RenderParticle_ColorSizeAngle( pIterator->GetParticleDraw(), tRenderPos, color, alpha * GetAlphaDistanceFade(tRenderPos, 10, 30), // Alpha m_ParticleRadius, pParticle->m_CurRotation ); pParticle = (const SmokeGrenadeParticle*)pIterator->GetNext( sortKey ); } } void C_FuncSmokeVolume::SimulateParticles( CParticleSimulateIterator *pIterator ) { if ( m_CurrentDensity == 0 ) return; SmokeGrenadeParticle *pParticle = (SmokeGrenadeParticle*)pIterator->GetFirst(); while ( pParticle ) { pParticle->m_CurRotation += pParticle->m_RotationFactor * ( M_PI / 180.0f ) * m_RotationSpeed * pIterator->GetTimeDelta(); pParticle = (SmokeGrenadeParticle*)pIterator->GetNext(); } } void C_FuncSmokeVolume::NotifyRemove() { m_xCount = m_yCount = m_zCount = 0; } void C_FuncSmokeVolume::FillVolume() { Vector vPos; for(int x=0; x < m_xCount; x++) { for(int y=0; y < m_yCount; y++) { for(int z=0; z < m_zCount; z++) { vPos = GetSmokeParticlePos( x, y, z ); if(SmokeParticleInfo *pInfo = GetSmokeParticleInfo(x,y,z)) { int contents = GetWorldPointContents(vPos); if(contents & CONTENTS_SOLID) { pInfo->m_pParticle = NULL; } else { SmokeGrenadeParticle *pParticle = (SmokeGrenadeParticle*)m_ParticleEffect.AddParticle(sizeof(SmokeGrenadeParticle), m_MaterialHandle); if(pParticle) { pParticle->m_Pos = vPos; pParticle->m_ColorInterp = (unsigned char)((rand() * 255) / RAND_MAX); pParticle->m_RotationFactor = FRand( -1.0f, 1.0f ); // Rotation factor. pParticle->m_CurRotation = FRand( -m_RotationSpeed, m_RotationSpeed ); } #ifdef _DEBUG int testX, testY, testZ; int index = GetSmokeParticleIndex(x,y,z); GetParticleInfoXYZ(index, testX, testY, testZ); assert(testX == x && testY == y && testZ == z); #endif Vector vColor = EngineGetLightForPoint(vPos); pInfo->m_Color[0] = LinearToTexture( vColor.x ); pInfo->m_Color[1] = LinearToTexture( vColor.y ); pInfo->m_Color[2] = LinearToTexture( vColor.z ); // Cast some rays and if it's too close to anything, fade its alpha down. pInfo->m_FadeAlpha = 1; for(int i=0; i < NUM_FADE_PLANES; i++) { trace_t trace; WorldTraceLine(vPos, vPos + s_FadePlaneDirections[i] * 100, MASK_SOLID_BRUSHONLY, &trace); if(trace.fraction < 1.0f) { float dist = DotProduct(trace.plane.normal, vPos) - trace.plane.dist; if(dist < 0) { pInfo->m_FadeAlpha = 0; } else if(dist < m_ParticleRadius) { float alphaScale = dist / m_ParticleRadius; alphaScale *= alphaScale * alphaScale; pInfo->m_FadeAlpha *= alphaScale; } } } pInfo->m_pParticle = pParticle; pInfo->m_TradeIndex = -1; } } } } } }