Torque3D/Engine/source/environment/timeOfDay.cpp
2025-03-09 11:53:23 -05:00

748 lines
20 KiB
C++

//-----------------------------------------------------------------------------
// Copyright (c) 2012 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
#include "platform/platform.h"
#include "environment/timeOfDay.h"
#include "console/consoleTypes.h"
#include "core/stream/bitStream.h"
#include "T3D/gameBase/gameConnection.h"
#include "environment/sun.h"
#include "console/engineAPI.h"
#include "console/typeValidators.h"
TimeOfDayUpdateSignal TimeOfDay::smTimeOfDayUpdateSignal;
IMPLEMENT_CO_NETOBJECT_V1(TimeOfDay);
ConsoleDocClass( TimeOfDay,
"@brief Environmental object that triggers a day/night cycle in level.\n\n"
"@note TimeOfDay only works in Advanced Lighting with a Sub object or ScatterSky\n\n"
"@tsexample\n"
"new TimeOfDay(tod)\n"
"{\n"
" axisTilt = \"23.44\";\n"
" dayLength = \"120\";\n"
" startTime = \"0.15\";\n"
" time = \"0.15\";\n"
" play = \"0\";\n"
" azimuthOverride = \"572.958\";\n"
" dayScale = \"1\";\n"
" nightScale = \"1.5\";\n"
" position = \"598.399 550.652 196.297\";\n"
" rotation = \"1 0 0 0\";\n"
" scale = \"1 1 1\";\n"
" canSave = \"1\";\n"
" canSaveDynamicFields = \"1\";\n"
"};\n"
"@endtsexample\n\n"
"@ingroup enviroMisc"
);
TimeOfDay::TimeOfDay()
: mStartTimeOfDay( 0.5f ), // High noon
mDayLen( 120.0f ), // 2 minutes
mAxisTilt( 23.44f ), // 35 degree tilt
mAzimuth( 0.0f ),
mElevation( 0.0f ),
mTimeOfDay( 0.0f ), // initialized to StartTimeOfDay in onAdd
mDayScale( 1.0f ),
mPlay( true ),
mNightScale( 1.5f ),
mAnimateTime( 0.0f ),
mAnimateSpeed( 0.0f ),
mAnimate( false )
{
mNetFlags.set( Ghostable | ScopeAlways );
mTypeMask = EnvironmentObjectType;
// Sets the sun vector directly overhead for lightmap generation
// The value of mSunVector is grabbed by the terrain lighting stuff.
/*
F32 ele, azi;
ele = azi = TORADIANS(90);
MathUtils::getVectorFromAngles(mSunVector, azi, ele);
*/
mPrevElevation = 0;
mNextElevation = 0;
mAzimuthOverride = 1.0f;
_initColors();
}
TimeOfDay::~TimeOfDay()
{
}
bool TimeOfDay::setTimeOfDay( void *object, const char *index, const char *data )
{
TimeOfDay *tod = static_cast<TimeOfDay*>(object);
tod->setTimeOfDay( dAtof( data ) );
return false;
}
bool TimeOfDay::setPlay( void *object, const char *index, const char *data )
{
TimeOfDay *tod = static_cast<TimeOfDay*>(object);
tod->setPlay( dAtob( data ) );
return false;
}
bool TimeOfDay::setDayLength( void *object, const char *index, const char *data )
{
TimeOfDay *tod = static_cast<TimeOfDay*>(object);
F32 length = dAtof( data );
if( length != 0 )
tod->setDayLength( length );
return false;
}
void TimeOfDay::initPersistFields()
{
docsURL;
addGroup( "TimeOfDay" );
addFieldV( "axisTilt", TypeRangedF32, Offset( mAxisTilt, TimeOfDay ), &CommonValidators::DegreeRange,
"The angle in degrees between global equator and tropic." );
addProtectedFieldV( "dayLength", TypeRangedF32, Offset( mDayLen, TimeOfDay ), &setDayLength, &defaultProtectedGetFn, &CommonValidators::PositiveFloat,
"The length of a virtual day in real world seconds." );
addFieldV( "startTime", TypeRangedF32, Offset( mStartTimeOfDay, TimeOfDay ), &CommonValidators::NormalizedFloat,
"" );
addProtectedFieldV( "time", TypeRangedF32, Offset( mTimeOfDay, TimeOfDay ), &setTimeOfDay, &defaultProtectedGetFn, &CommonValidators::NormalizedFloat, "Current time of day." );
addProtectedField( "play", TypeBool, Offset( mPlay, TimeOfDay ), &setPlay, &defaultProtectedGetFn, "True when the TimeOfDay object is operating." );
addFieldV( "azimuthOverride", TypeRangedF32, Offset( mAzimuthOverride, TimeOfDay ), &CommonValidators::PosDegreeRange, "" );
addFieldV( "dayScale", TypeRangedF32, Offset( mDayScale, TimeOfDay ), &CommonValidators::PositiveFloat, "Scalar applied to time that elapses while the sun is up." );
addFieldV( "nightScale", TypeRangedF32, Offset( mNightScale, TimeOfDay ), &CommonValidators::PositiveFloat, "Scalar applied to time that elapses while the sun is down." );
endGroup( "TimeOfDay" );
Parent::initPersistFields();
}
void TimeOfDay::consoleInit()
{
Parent::consoleInit();
//addVariable( "$TimeOfDay::currentTime", &TimeOfDay::smCurrentTime );
//addVariable( "$TimeOfDay::timeScale", TypeF32, &TimeOfDay::smTimeScale );
}
void TimeOfDay::inspectPostApply()
{
_updatePosition();
setMaskBits( OrbitMask );
}
void TimeOfDay::_onGhostAlwaysDone()
{
_updatePosition();
}
bool TimeOfDay::onAdd()
{
if ( !Parent::onAdd() )
return false;
// The server initializes to the specified starting values.
// The client initializes itself to the server time from
// unpackUpdate.
if ( isServerObject() )
{
mTimeOfDay = mStartTimeOfDay;
_updatePosition();
}
// We don't use a bounds.
setGlobalBounds();
resetWorldBox();
addToScene();
// Lets receive ghost events so we can resolve
// the sun object.
if ( isClientObject() )
NetConnection::smGhostAlwaysDone.notify( this, &TimeOfDay::_onGhostAlwaysDone );
if ( isServerObject() )
Con::executef( this, "onAdd" );
setProcessTick( true );
return true;
}
void TimeOfDay::onRemove()
{
if ( isClientObject() )
NetConnection::smGhostAlwaysDone.remove( this, &TimeOfDay::_onGhostAlwaysDone );
removeFromScene();
Parent::onRemove();
}
U32 TimeOfDay::packUpdate(NetConnection *conn, U32 mask, BitStream *stream )
{
U32 retMask = Parent::packUpdate( conn, mask, stream );
if ( stream->writeFlag( mask & OrbitMask ) )
{
stream->write( mStartTimeOfDay );
stream->write( mDayLen );
stream->write( mTimeOfDay );
stream->write( mAxisTilt );
stream->write( mAzimuthOverride );
stream->write( mDayScale );
stream->write( mNightScale );
stream->writeFlag( mPlay );
}
if ( stream->writeFlag( mask & AnimateMask ) )
{
stream->write( mAnimateTime );
stream->write( mAnimateSpeed );
}
return retMask;
}
void TimeOfDay::unpackUpdate( NetConnection *conn, BitStream *stream )
{
Parent::unpackUpdate( conn, stream );
if ( stream->readFlag() ) // OrbitMask
{
stream->read( &mStartTimeOfDay );
stream->read( &mDayLen );
stream->read( &mTimeOfDay );
stream->read( &mAxisTilt );
stream->read( &mAzimuthOverride );
stream->read( &mDayScale );
stream->read( &mNightScale );
mPlay = stream->readFlag();
_updatePosition();
}
if ( stream->readFlag() ) // AnimateMask
{
F32 time, speed;
stream->read( &time );
stream->read( &speed );
if( isProperlyAdded() )
animate( time, speed );
}
}
void TimeOfDay::processTick( const Move *move )
{
if ( mAnimate )
{
F32 current = mTimeOfDay * 360.0f;
F32 next = current + (mAnimateSpeed * TickSec);
// Protect for wrap around.
while ( next > 360.0f )
next -= 360.0f;
// Clamp to make sure we don't pass the target time.
if ( next >= mAnimateTime )
{
next = mAnimateTime;
mAnimate = false;
}
// Set the new time of day.
mTimeOfDay = next / 360.0f;
_updatePosition();
_updateTimeEvents();
if ( !mAnimate && isServerObject() )
Con::executef( this, "onAnimateDone" );
}
else if ( mPlay )
{
F32 dt = TickSec;
F32 current = mRadToDeg( mNextElevation );
if ( current > 350.0f || ( 0.0f <= current && current < 190.0f ) )
dt *= mDayScale;
else
dt *= mNightScale;
mTimeOfDay += dt / mDayLen;
// It could be possible for more than a full day to
// pass in a single advance time, so I put this inside a loop
// but timeEvents will not actually be called for the
// skipped day.
while ( mTimeOfDay > 1.0f )
mTimeOfDay -= 1.0f;
_updatePosition();
_updateTimeEvents();
}
else
_updatePosition();
}
void TimeOfDay::_updatePosition()
{
mPrevElevation = mNextElevation;
if ( mFabs( mAzimuthOverride ) )
{
mElevation = mDegToRad( mTimeOfDay * 360.0f );
mAzimuth = mAzimuthOverride;
mNextElevation = mElevation; // already normalized
}
else
{
//// Full azimuth/elevation calculation.
//// calculate sun decline and meridian angle (in radians)
//F32 sunDecline = mSin( M_2PI * mTimeOfYear ) * mDegToRad( mAxisTilt );
//F32 meridianAngle = mTimeOfDay * M_2PI - mDegToRad( mLongitude );
//// calculate the elevation and azimuth (in radians)
//mElevation = _calcElevation( mDegToRad( mLatitude ), sunDecline, meridianAngle );
//mAzimuth = _calcAzimuth( mDegToRad( mLatitude ), sunDecline, meridianAngle );
// Simplified azimuth/elevation calculation.
// calculate sun decline and meridian angle (in radians)
F32 sunDecline = mDegToRad( mAxisTilt );
F32 meridianAngle = mTimeOfDay * M_2PI;
// calculate the elevation and azimuth (in radians)
mElevation = _calcElevation( 0.0f, sunDecline, meridianAngle );
mAzimuth = _calcAzimuth( 0.0f, sunDecline, meridianAngle );
// calculate 'normalized' elevation (0=sunrise, PI/2=zenith, PI=sunset, 3PI/4=nadir)
F32 normElevation = M_PI_F * mElevation / ( 2 * _calcElevation( 0.0f, sunDecline, 0.0f ) );
if ( mAzimuth > M_PI_F )
normElevation = M_PI_F - normElevation;
else if ( mElevation < 0 )
normElevation = M_2PI_F + normElevation;
mNextElevation = normElevation;
}
// Only the client updates the sun position!
if ( isClientObject() )
smTimeOfDayUpdateSignal.trigger( this, mTimeOfDay );
}
F32 TimeOfDay::_calcElevation( F32 lat, F32 dec, F32 mer )
{
return mAsin( mSin(lat) * mSin(dec) + mCos(lat) * mCos(dec) * mCos(mer) );
}
F32 TimeOfDay::_calcAzimuth( F32 lat, F32 dec, F32 mer )
{
// Add PI to normalize this from the range of -PI/2 to PI/2 to 0 to 2 * PI;
return mAtan2( mSin(mer), mCos(mer) * mSin(lat) - mTan(dec) * mCos(lat) ) + M_PI_F;
}
void TimeOfDay::_getSunColor( LinearColorF *outColor ) const
{
const COLOR_TARGET *ct = NULL;
F32 ele = mClampF( M_2PI_F - mNextElevation, 0.0f, M_PI_F );
F32 phase = -1.0f;
F32 div;
if (!mColorTargets.size())
{
outColor->set(1.0f,1.0f,1.0f);
return;
}
if (mColorTargets.size() == 1)
{
ct = &mColorTargets[0];
outColor->set(ct->color.red, ct->color.green, ct->color.blue);
return;
}
//simple check
if ( mColorTargets[0].elevation != 0.0f )
{
AssertFatal(0, "TimeOfDay::GetColor() - First elevation must be 0.0 radians");
outColor->set(1.0f, 1.0f, 1.0f);
//mBandMod = 1.0f;
//mCurrentBandColor = color;
return;
}
if ( mColorTargets[mColorTargets.size()-1].elevation != M_PI_F )
{
AssertFatal(0, "Celestails::GetColor() - Last elevation must be PI");
outColor->set(1.0f, 1.0f, 1.0f);
//mBandMod = 1.0f;
//mCurrentBandColor = color;
return;
}
//we need to find the phase and interp... also loop back around
U32 count=0;
for (;count < mColorTargets.size() - 1; count++)
{
const COLOR_TARGET *one = &mColorTargets[count];
const COLOR_TARGET *two = &mColorTargets[count+1];
if (ele >= one->elevation && ele <= two->elevation)
{
div = two->elevation - one->elevation;
//catch bad input divide by zero
if ( mFabs( div ) < 0.01f )
div = 0.01f;
phase = (ele - one->elevation) / div;
outColor->interpolate( one->color, two->color, phase );
//mCurrentBandColor.interpolate(one->bandColor, two->bandColor, phase);
//mBandMod = one->bandMod * (1.0f - phase) + two->bandMod * phase;
return;
}
}
AssertFatal(0,"This isn't supposed to happen");
}
void TimeOfDay::_initColors()
{
// NOTE: The elevation targets represent distances
// from PI/2 radians (strait up).
LinearColorF c;
LinearColorF bc;
// e is for elevation
F32 e = M_PI_F / 13.0f; // (semicircle in radians)/(number of color target entries);
// Day
c.set(1.0f,1.0f,1.0f);
_addColorTarget(0, c, 1.0f, c); // High noon at equanox
c.set(.9f,.9f,.9f);
_addColorTarget(e * 1.0f, c, 1.0f, c);
c.set(.9f,.9f,.9f);
_addColorTarget(e * 2.0f, c, 1.0f, c);
c.set(.8f,.75f,.75f);
_addColorTarget(e * 3.0f, c, 1.0f, c);
c.set(.7f,.65f,.65f);
_addColorTarget(e * 4.0f, c, 1.0f, c);
//Dawn and Dusk (3 entries)
c.set(.7f,.65f,.65f);
bc.set(.8f,.6f,.3f);
_addColorTarget(e * 5.0f, c, 3.0f, bc);
c.set(.65f,.54f,.4f);
bc.set(.75f,.5f,.4f);
_addColorTarget(e * 6.0f, c, 2.75f, bc);
c.set(.55f,.45f,.25f);
bc.set(.65f,.3f,.3f);
_addColorTarget(e * 7.0f, c, 2.5f, bc);
//NIGHT
c.set(.3f,.3f,.3f);
bc.set(.7f,.4f,.2f);
_addColorTarget(e * 8.0f, c, 1.25f, bc);
c.set(.25f,.25f,.3f);
bc.set(.8f,.3f,.2f);
_addColorTarget(e * 9.0f, c, 1.00f, bc);
c.set(.25f,.25f,.4f);
_addColorTarget(e * 10.0f, c, 1.0f, c);
c.set(.2f,.2f,.35f);
_addColorTarget(e * 11.0f, c, 1.0f, c);
c.set(.15f,.15f,.2f);
_addColorTarget(M_PI_F, c, 1.0f, c); // Midnight at equanox.
}
void TimeOfDay::_addColorTarget( F32 ele, const LinearColorF &color, F32 bandMod, const LinearColorF &bandColor )
{
COLOR_TARGET newTarget;
newTarget.elevation = ele;
newTarget.color = color;
newTarget.bandMod = bandMod;
newTarget.bandColor = bandColor;
mColorTargets.push_back(newTarget);
}
void TimeOfDay::_updateTimeEvents()
{
if ( mTimeEvents.empty() )
return;
// Get the prev, next elevation in degrees since TimeOfDayEvent is specified
// in degrees.
F32 prevElevation = mRadToDeg( mPrevElevation );
F32 nextElevation = mRadToDeg( mNextElevation );
// If prevElevation is less than nextElevation then its the next day.
// Unroll it so we can just loop forward in time and simplify our loop.
if ( nextElevation < prevElevation )
nextElevation += 360.0f;
const U32 evtCount = mTimeEvents.size();
// Find where in the event list we need to start...
// The first timeEvent with elevation greater than our previous elevation.
U32 start = 0;
for ( ; start < evtCount; start++ )
{
if ( mTimeEvents[start].triggerElevation > prevElevation )
break;
}
bool onNextDay = false;
// Nothing between prevElevation and the end of the day...
// Check between start of the day and nextElevation...
if ( start == evtCount )
{
start = 0;
for ( ; start < evtCount; start++ )
{
if ( mTimeEvents[start].triggerElevation <= nextElevation )
{
onNextDay = true;
break;
}
}
}
// No events were hit...
if ( start == evtCount )
return;
U32 itr = start;
while ( true )
{
TimeOfDayEvent &timeEvent = mTimeEvents[itr];
F32 elev = timeEvent.triggerElevation;
if ( onNextDay )
elev += 360.0f;
// Hit an event that happens later after nextElevation so we
// have checked everything within the range and are done.
if ( elev > nextElevation )
break;
// If its not greater than the nextElevation it must be less, and if
// we are here we already know its greater than prevElevation.
AssertFatal( elev >= prevElevation && elev <= nextElevation, "TimeOfDay::_updateTimeEvents - Logical error in here!" );
AssertFatal( !timeEvent.deleteMe, "TimeOfDay::_updateTimeEvents - tried to fire the same event twice!" );
_onTimeEvent( timeEvent.identifier );
if ( timeEvent.oneShot )
timeEvent.deleteMe = true;
// On to the next time event...
itr++;
// We hit the end of the day?
if ( itr == evtCount )
{
// We are already on the next day so we have checked everything.
if ( onNextDay )
break;
// Check events for the next day
else
{
itr = 0;
onNextDay = true;
}
}
}
// Cleanup one-shot events that fired...
for ( S32 i = 0; i < mTimeEvents.size(); i++ )
{
if ( mTimeEvents[i].deleteMe )
{
// Don't use erase_fast, there are ordered.
mTimeEvents.erase( i );
i--;
}
}
}
void TimeOfDay::addTimeEvent( F32 triggerElevation, const UTF8 *identifier )
{
// Insert in ascending order of elevation.
// Note that having more than one TimeEvent with the same triggerElevation
// may cause undefined behavior.
TimeOfDayEvent *pEvent = NULL;
if ( mTimeEvents.empty() || mTimeEvents.last().triggerElevation <= triggerElevation )
{
mTimeEvents.increment();
pEvent = &mTimeEvents.last();
}
else
{
for ( S32 i = 0; i < mTimeEvents.size(); i++ )
{
if ( mTimeEvents[i].triggerElevation > triggerElevation )
{
mTimeEvents.insert( i );
pEvent = &mTimeEvents[i];
break;
}
}
}
AssertFatal( pEvent, "TimeOfDay::addTimeEvent - could not find place to insert event." );
pEvent->triggerElevation = triggerElevation;
pEvent->identifier = identifier;
pEvent->oneShot = false;
pEvent->deleteMe = false;
}
void TimeOfDay::setTimeOfDay( F32 time )
{
mTimeOfDay = time;
while ( mTimeOfDay > 1.0f )
mTimeOfDay -= 1.0f;
while ( mTimeOfDay < 0.0f )
mTimeOfDay += 1.0f;
_updatePosition();
//if ( isServerObject() )
_updateTimeEvents();
setMaskBits( OrbitMask );
}
void TimeOfDay::_onTimeEvent( const String &identifier )
{
// Client doesn't do onTimeEvent callbacks.
if ( isClientObject() )
return;
String strCurrentTime = String::ToString( "%g", mTimeOfDay );
F32 elevation = mRadToDeg( mNextElevation );
while( elevation < 0 )
elevation += 360.0f;
while( elevation > 360.0f )
elevation -= 360.0f;
String strCurrentElevation = String::ToString( "%g", elevation );
Con::executef( this, "onTimeEvent", identifier.c_str(), strCurrentTime.c_str(), strCurrentElevation.c_str() );
}
void TimeOfDay::animate( F32 time, F32 speed )
{
// Stop any existing animation... this one
// becomes the new one.
mAnimate = false;
// Set the target time to hit.
mAnimateTime = mClamp(time, 0.0f, 360.0f);
F32 current = mTimeOfDay * 360.0f;
F32 target = mAnimateTime;
if ( target < current )
target += 360.0f;
// If we're already at the current time then
// we have nothing more to do... the animation is here.
F32 dif = target - current;
if ( mIsZero( dif ) )
return;
// Start playback.
mAnimateSpeed = speed;
mAnimate = true;
if ( isServerObject() )
{
Con::executef( this, "onAnimateStart" );
setMaskBits( AnimateMask );
}
}
DefineEngineMethod( TimeOfDay, addTimeOfDayEvent, void, (F32 elevation, const char *identifier ),,
"" )
{
object->addTimeEvent( elevation, identifier );
}
DefineEngineMethod( TimeOfDay, setTimeOfDay, void, ( F32 time ),,
"" )
{
object->setTimeOfDay( time );
}
DefineEngineMethod( TimeOfDay, setPlay, void, ( bool enabled ),,
"")
{
object->setPlay( enabled );
}
DefineEngineMethod( TimeOfDay, setDayLength, void, ( F32 seconds ),,
"" )
{
if ( seconds > 0.0f )
object->setDayLength( seconds );
}
DefineEngineMethod( TimeOfDay, animate, void, ( F32 elevation, F32 degreesPerSecond ),,
"")
{
object->animate( elevation, degreesPerSecond );
}