Physically-Based Clock Rendering
6-10 minutesRecently I spent a few moments implementing physical clocks in Lofty Lagoon. Using old and new code.
Let's dig a little into keeping track of time in the world of a video game.
The Sky is the Limit
Disclaimer: I was joking when I said "Physically-Based", it's better to consider them physically present in the game world.
One of the first entities I made for Lofty Lagoon was the sky entity.
It's currently used to drive the sky rendering pass and keeps track of time.
Its earlier versions only rendered a basic mesh with a simple shader that tried its best to render an interesting background.
It came with some interesting looking spirals, these were actually bugs. At the time I was relying on the texture coordinates of the sky's sphere model to map a tiling texture.
A few months later, I put together a more convincing looking sky. At this point the it also relied on a time of day variable which was used to update the sun's direction vector in the game code.
Originally this time of day variable was just fed through some functions to make the direction vector spin the sun around over time. As time progressed, so did the time-keeping code.
Nowadays the sun doesn't use the time of day variable directly. It instead remaps it to a small set of preset values that determine the final sun angle.
// Angle of the sun at 7 AM
constexpr double Morning = 0.0;
constexpr double Midday = 0.45; // At 12PM.
constexpr double Evening = 0.8; // 7PM.
constexpr double Midnight = 1.5; // 12AM.
double Angle = 0.0f;
// Remap the time of day to the angle of the sun.
if( TimeCode.Time > 0.0 && TimeCode.Time < 7.0 )
{
Angle = Math::Map( TimeCode.Time, 0.0, 7.0, -0.5, Morning );
}
else if( TimeCode.Time > 7.0 && TimeCode.Time < 12.0 )
{
Angle = Math::Map( TimeCode.Time, 7.0, 12.0, Morning, Midday );
}
else if( TimeCode.Time > 12.0 && TimeCode.Time < 19.0 )
{
Angle = Math::Map( TimeCode.Time, 12.0, 19.0, Midday, Evening );
}
else if( TimeCode.Time > 19.0 && TimeCode.Time < 24.0 )
{
Angle = Math::Map( TimeCode.Time, 19.0, 24.0, Evening, Midnight );
}
The sun preset values themselves are artist controlled, as in... I'm the artist, and I've defined them in the code.
You can maybe tell that the time of day value ranges from 0.0
to 24.0
. I don't let the time of day value exceed 24 hours because I don't want it to keep accumulating as I expect it to introduce precision errors when it has been counting for a long time.
const auto UnwrappedTimeOfDay = TimeCode.Time;
// 24-hour day.
TimeCode.Time = fmod( TimeCode.Time, 24.0 );
// If the day got wrapped around to the next one, the time of day will be less than the original value after wrapping.
if( TimeCode.Time < UnwrappedTimeOfDay )
{
TimeCode.Day++;
}
When the time ends up wrapping back around zero, I increment a counter that tracks how many days have passed. It's a pretty simplistic solution; and if the lore calls for it, I'll likely expand it into a system that counts months and years as well.
Who knows what kind of whack calendar I'll suddenly end up with!
Moving to Physical Clocks
Up until now, the exact time of day was communicated to the player via an on-screen UI element that displayed the hours, minutes and days, that the sky entity's time had been running for.
On its own, it isn't all that useful, and having it visible at all times is kind of a waste of screen space. These are justifications I am making after the fact. Because the reality is...
I just randomly felt like adding physical clocks into the game. A very controversial statement, clearly.
It ended up being fairly simple, but I did run into a couple of small issues along the way.
I decided to use a rig to drive the clock, because I didn't want to render a bunch of individual pieces. I could possibly have done all of it in a shader, but my goal was to try and manipulate the bones directly.
The animation code in my engine has always been a bit hit or miss, so I wanted to try driving the bones to see what would happen.
Surprisingly, the animation system didn't explode this time. I'll be very suspicious of the stability of these clocks whenever I update their models again.
To drive the rig, I wrote a clock entity; which is responsible for updating the bone transformations of the hands.
I also created a structure called TimeCode
which now stores the actual time of day information. Previously, the time of day was just stored in a double
named TimeOfDay
.
This TimeCode
structure has some additional functions that make it easy to check how much time has passed.
These were originally present in the sky entity and were calculated inline to display the on-screen UI element.
int TimeCode::Hours() const
{
return static_cast<int>( floor( Time ) );
}
int TimeCode::Minutes() const
{
return static_cast<int>( floor( ( Time - Hours() ) * 60.0f ) );
}
int TimeCode::Seconds() const
{
return static_cast<int>( floor( ( Time - Hours() ) * 60.0f * 60.0f ) ) % 60;
}
With these utility functions in place, it is now time to drive the rig.
const auto SecondsHandle = AnimationInstance.GetBoneIndex( "Seconds" );
if( SecondsHandle > -1 )
{
const auto Rotation = Matrix4D::RotationZ(
( static_cast<float>( TimeCode.Seconds() ) / 60.0f ) * Math::Pi2() * -1.0f
);
auto& GlobalTransform = AnimationInstance.Bones[SecondsHandle].GlobalTransform;
GlobalTransform = Skeleton.Bones[SecondsHandle].BoneToModel * Rotation;
AnimationInstance.Bones[SecondsHandle].BoneTransform = GlobalTransform * Skeleton.Bones[SecondsHandle].ModelToBone;
}
In the clock entity, I fetch the bone index of a bone named "Seconds"
. This bone is use to move all vertices associated with the seconds hand.
I then create a rotation matrix using the timecode's seconds value.
Currently there aren't any functions to quickly override bone transformations in Shatter, so I instead manipulate the bone transformation directly.
It first applies the rotation, the transforms it into model space for the overall transformation, then for the actual bone transformation, the bone's inverse matrix is combined with that overall transformation.
Doing this for each hand, gets us our physical in-world clock.
To make it tick, I quickly added in this additional snippet which makes the clock play a sound every 2 in-game seconds.
const auto TimeOfDay = CurrentGameSession().Get( "timeofday" ).Value.GetFloat();
TimeCode TimeCode;
TimeCode.Time = TimeOfDay;
const auto Delta = abs( TimeCode.Seconds() - Previous.Seconds() );
if( TickingSound && Delta >= 2 )
{
Spatial Information = Spatial::Create();
Information.Position = Transform.GetPosition() + AnimationInstance.GetBonePosition( "Seconds" );
Information.Is3D = true;
Information.DelayByDistance = true;
Information.Rate = 6.0f;
Information.MinimumDistance = 2.0f;
TickingSound->Start( Information );
Previous.Time = TimeCode.Time;
}
I hope this was a somewhat fun read for those of you that made it all this way.
Feel free to share your thoughts, either by sending me an e-mail or joining me in the Lofty Lagoon Discord!
(or whatever other platforms I'm on)
The March of The Mountains (Lofty Lagoon)