summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--game/environment.cpp94
-rw-r--r--game/environment.h18
-rw-r--r--game/gamestate.cpp3
-rw-r--r--game/gamestate.h2
-rw-r--r--lib/chronology.cpp12
-rw-r--r--lib/chronology.h2
-rw-r--r--lib/maths.h4
-rw-r--r--test/Jamfile.jam1
-rw-r--r--test/test-environment.cpp35
-rw-r--r--test/test-render.cpp8
-rw-r--r--ui/gameMainWindow.cpp8
11 files changed, 183 insertions, 4 deletions
diff --git a/game/environment.cpp b/game/environment.cpp
new file mode 100644
index 0000000..19aad84
--- /dev/null
+++ b/game/environment.cpp
@@ -0,0 +1,94 @@
+#include "environment.h"
+#include <chronology.h>
+#include <gfx/gl/sceneRenderer.h>
+
+Environment::Environment() : worldTime {"2024-01-01T12:00:00"_time_t} { }
+
+void
+Environment::tick(TickDuration)
+{
+ worldTime += 50;
+}
+
+void
+Environment::render(const SceneRenderer & renderer, const SceneProvider & scene) const
+{
+ constexpr RGB baseAmbient {0.1F}, baseDirectional {0.0F};
+ constexpr RGB relativeAmbient {0.3F, 0.3F, 0.4F}, relativeDirectional {0.6F, 0.6F, 0.5F};
+
+ const auto sunPos = getSunPos({}, worldTime);
+ const auto sunDir = (glm::mat3 {rotate_yp({sunPos.y + pi, sunPos.x})} * north);
+ const auto vertical = -std::min(0.F, sunDir.z - 0.1F);
+ const auto ambient = baseAmbient + relativeAmbient * vertical;
+ const auto directional = baseDirectional + relativeDirectional * vertical;
+
+ renderer.setAmbientLight(ambient);
+ renderer.setDirectionalLight(directional, sunDir, scene);
+}
+
+// Based on the C++ code published at https://www.psa.es/sdg/sunpos.htm
+// Linked from https://www.pveducation.org/pvcdrom/properties-of-sunlight/suns-position-to-high-accuracy
+Direction2D
+Environment::getSunPos(const Direction2D position, const time_t time)
+{
+ auto & longitude = position.x;
+ auto & latitude = position.y;
+ using std::acos;
+ using std::asin;
+ using std::atan2;
+ using std::cos;
+ using std::floor;
+ using std::sin;
+ using std::tan;
+ static const auto JD2451545 = "2000-01-01T12:00:00"_time_t;
+
+ // Calculate difference in days between the current Julian Day
+ // and JD 2451545.0, which is noon 1 January 2000 Universal Time
+ // Calculate time of the day in UT decimal hours
+ const auto dDecimalHours = static_cast<float>(time % 86400) / 3600.F;
+ const auto dElapsedJulianDays = static_cast<float>(time - JD2451545) / 86400.F;
+
+ // Calculate ecliptic coordinates (ecliptic longitude and obliquity of the
+ // ecliptic in radians but without limiting the angle to be less than 2*Pi
+ // (i.e., the result may be greater than 2*Pi)
+ const auto dOmega = 2.1429F - 0.0010394594F * dElapsedJulianDays;
+ const auto dMeanLongitude = 4.8950630F + 0.017202791698F * dElapsedJulianDays; // Radians
+ const auto dMeanAnomaly = 6.2400600F + 0.0172019699F * dElapsedJulianDays;
+ const auto dEclipticLongitude = dMeanLongitude + 0.03341607F * sin(dMeanAnomaly)
+ + 0.00034894F * sin(2 * dMeanAnomaly) - 0.0001134F - 0.0000203F * sin(dOmega);
+ const auto dEclipticObliquity = 0.4090928F - 6.2140e-9F * dElapsedJulianDays + 0.0000396F * cos(dOmega);
+
+ // Calculate celestial coordinates ( right ascension and declination ) in radians
+ // but without limiting the angle to be less than 2*Pi (i.e., the result may be
+ // greater than 2*Pi)
+ const auto dSin_EclipticLongitude = sin(dEclipticLongitude);
+ const auto dY = cos(dEclipticObliquity) * dSin_EclipticLongitude;
+ const auto dX = cos(dEclipticLongitude);
+ auto dRightAscension = atan2(dY, dX);
+ if (dRightAscension < 0) {
+ dRightAscension = dRightAscension + two_pi;
+ }
+ const auto dDeclination = asin(sin(dEclipticObliquity) * dSin_EclipticLongitude);
+
+ // Calculate local coordinates ( azimuth and zenith angle ) in degrees
+ const auto dGreenwichMeanSiderealTime = 6.6974243242F + 0.0657098283F * dElapsedJulianDays + dDecimalHours;
+ const auto dLocalMeanSiderealTime
+ = (dGreenwichMeanSiderealTime * 15.0F + (longitude / degreesToRads)) * degreesToRads;
+ const auto dHourAngle = dLocalMeanSiderealTime - dRightAscension;
+ const auto dLatitudeInRadians = latitude;
+ const auto dCos_Latitude = cos(dLatitudeInRadians);
+ const auto dSin_Latitude = sin(dLatitudeInRadians);
+ const auto dCos_HourAngle = cos(dHourAngle);
+ Direction2D udtSunCoordinates;
+ udtSunCoordinates.y
+ = (acos(dCos_Latitude * dCos_HourAngle * cos(dDeclination) + sin(dDeclination) * dSin_Latitude));
+ udtSunCoordinates.x = atan2(-sin(dHourAngle), tan(dDeclination) * dCos_Latitude - dSin_Latitude * dCos_HourAngle);
+ if (udtSunCoordinates.x < 0) {
+ udtSunCoordinates.x = udtSunCoordinates.x + two_pi;
+ }
+ // Parallax Correction
+ const auto dParallax = (earthMeanRadius / astronomicalUnit) * sin(udtSunCoordinates.y);
+ udtSunCoordinates.y = half_pi - (udtSunCoordinates.y + dParallax);
+
+ return udtSunCoordinates;
+}
diff --git a/game/environment.h b/game/environment.h
new file mode 100644
index 0000000..a6f3036
--- /dev/null
+++ b/game/environment.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "config/types.h"
+#include "worldobject.h"
+
+class SceneRenderer;
+class SceneProvider;
+
+class Environment : public WorldObject {
+public:
+ Environment();
+ void tick(TickDuration elapsed) override;
+ void render(const SceneRenderer &, const SceneProvider &) const;
+ static Direction2D getSunPos(const Direction2D position, const time_t time);
+
+private:
+ time_t worldTime;
+};
diff --git a/game/gamestate.cpp b/game/gamestate.cpp
index fcd4248..910e8a7 100644
--- a/game/gamestate.cpp
+++ b/game/gamestate.cpp
@@ -1,4 +1,5 @@
#include "gamestate.h"
+#include "environment.h"
#include <cassert>
GameState * gameState {nullptr};
@@ -7,6 +8,8 @@ GameState::GameState()
{
assert(!gameState);
gameState = this;
+
+ environment = world.create<Environment>();
}
GameState::~GameState()
diff --git a/game/gamestate.h b/game/gamestate.h
index f07f844..892aa69 100644
--- a/game/gamestate.h
+++ b/game/gamestate.h
@@ -7,6 +7,7 @@
class WorldObject;
class GeoData;
+class Environment;
class GameState {
public:
@@ -17,6 +18,7 @@ public:
Collection<WorldObject> world;
std::shared_ptr<GeoData> geoData;
+ std::shared_ptr<Environment> environment;
AssetFactory::Assets assets;
};
diff --git a/lib/chronology.cpp b/lib/chronology.cpp
new file mode 100644
index 0000000..8707bba
--- /dev/null
+++ b/lib/chronology.cpp
@@ -0,0 +1,12 @@
+#include "chronology.h"
+
+time_t
+operator""_time_t(const char * iso, size_t)
+{
+ struct tm tm {};
+
+ if (const auto end = strptime(iso, "%FT%T", &tm); !end || *end) {
+ throw std::invalid_argument("Invalid date");
+ }
+ return mktime(&tm);
+}
diff --git a/lib/chronology.h b/lib/chronology.h
index 1980116..688a1f7 100644
--- a/lib/chronology.h
+++ b/lib/chronology.h
@@ -1,5 +1,7 @@
#pragma once
#include <chrono>
+#include <ctime>
using TickDuration = std::chrono::duration<float, std::chrono::seconds::period>;
+time_t operator""_time_t(const char * iso, size_t);
diff --git a/lib/maths.h b/lib/maths.h
index 018ef0e..3127d3c 100644
--- a/lib/maths.h
+++ b/lib/maths.h
@@ -42,6 +42,10 @@ constexpr auto half_pi {glm::half_pi<float>()};
constexpr auto quarter_pi {half_pi / 2};
constexpr auto pi {glm::pi<float>()};
constexpr auto two_pi {glm::two_pi<float>()};
+constexpr auto degreesToRads = pi / 180.F;
+
+constexpr auto earthMeanRadius = 6371.01F; // In km
+constexpr auto astronomicalUnit = 149597890.F; // In km
template<glm::length_t D>
constexpr inline GlobalPosition<D>
diff --git a/test/Jamfile.jam b/test/Jamfile.jam
index 0b830a8..3ab4c4c 100644
--- a/test/Jamfile.jam
+++ b/test/Jamfile.jam
@@ -63,6 +63,7 @@ run test-instancing.cpp : -- : test-glContainer : <library>test ;
run perf-instancing.cpp : \< : test-instancing : <library>benchmark <library>test ;
run test-glContainer.cpp : : : <library>test ;
run test-pack.cpp : : : <library>test ;
+run test-environment.cpp : : : <library>test ;
compile test-static-enumDetails.cpp ;
compile test-static-stream_support.cpp ;
explicit perf-assetFactory ;
diff --git a/test/test-environment.cpp b/test/test-environment.cpp
new file mode 100644
index 0000000..b6e0e4f
--- /dev/null
+++ b/test/test-environment.cpp
@@ -0,0 +1,35 @@
+#define BOOST_TEST_MODULE environment
+#include <boost/test/data/test_case.hpp>
+#include <boost/test/unit_test.hpp>
+#include <cmath>
+#include <stream_support.h>
+
+#include <chronology.h>
+#include <config/types.h>
+#include <game/environment.h>
+#include <maths.h>
+
+using sunPosTestData = std::tuple<Direction2D, time_t, Direction2D>;
+constexpr Direction2D Doncaster = {-1.1, 53.5};
+constexpr Direction2D NewYork = {74.0, 40.7};
+constexpr Direction2D Syndey = {-151.2, -33.9};
+constexpr Direction2D EqGM = {};
+
+BOOST_DATA_TEST_CASE(sun_position,
+ boost::unit_test::data::make<sunPosTestData>({
+ {EqGM, "2024-01-02T00:00:00"_time_t, {181.52F, -66.86F}},
+ {EqGM, "2024-01-02T06:00:00"_time_t, {113.12F, -0.85F}},
+ {EqGM, "2024-01-02T12:00:00"_time_t, {177.82F, 66.97F}},
+ {EqGM, "2024-01-02T18:00:00"_time_t, {246.99F, 0.90F}},
+ {EqGM, "2024-01-03T00:00:00"_time_t, {181.52F, -67.04F}},
+ {EqGM, "2024-06-29T12:00:00"_time_t, {2.1F, 66.80F}},
+ {Doncaster, "2024-06-29T12:00:00"_time_t, {176.34F, 59.64F}},
+ {NewYork, "2024-06-29T12:00:00"_time_t, {278.04F, 27.34F}},
+ {Syndey, "2024-06-29T12:00:00"_time_t, {106.13F, -63.29F}},
+ }),
+ position, timeOfYear, expSunPos)
+{
+ const auto sunPos = Environment::getSunPos(position * degreesToRads, timeOfYear) / degreesToRads;
+ BOOST_CHECK_CLOSE(sunPos.x, expSunPos.x, 1.F);
+ BOOST_CHECK_CLOSE(sunPos.y, expSunPos.y, 1.F);
+}
diff --git a/test/test-render.cpp b/test/test-render.cpp
index b9a809e..ea53708 100644
--- a/test/test-render.cpp
+++ b/test/test-render.cpp
@@ -1,3 +1,4 @@
+#include "game/environment.h"
#define BOOST_TEST_MODULE test_render
#include "testHelpers.h"
@@ -33,6 +34,7 @@ class TestScene : public SceneProvider {
std::shared_ptr<Plant> plant1;
RailLinks rail;
std::shared_ptr<GeoData> gd = std::make_shared<GeoData>(GeoData::createFlat({0, 0}, {1000000, 1000000}, 1));
+ std::shared_ptr<Environment> env = std::make_shared<Environment>();
Terrain terrain {gd};
Water water {gd};
@@ -69,6 +71,12 @@ public:
}
void
+ environment(const SceneShader &, const SceneRenderer & r) const override
+ {
+ env->render(r, *this);
+ }
+
+ void
shadows(const ShadowMapper & shadowMapper) const override
{
terrain.shadows(shadowMapper);
diff --git a/ui/gameMainWindow.cpp b/ui/gameMainWindow.cpp
index 6168504..c53300b 100644
--- a/ui/gameMainWindow.cpp
+++ b/ui/gameMainWindow.cpp
@@ -1,16 +1,17 @@
#include "gameMainWindow.h"
#include "editNetwork.h"
#include "gameMainSelector.h"
-#include "gfx/camera_controller.h"
#include "manualCameraController.h"
#include "modeHelper.h"
#include "toolbar.h"
#include "window.h"
#include <SDL2/SDL.h>
#include <collection.h>
+#include <game/environment.h>
#include <game/gamestate.h>
#include <game/network/rail.h>
#include <game/worldobject.h> // IWYU pragma: keep
+#include <gfx/camera_controller.h>
#include <gfx/renderable.h>
#include <glad/gl.h>
#include <glm/glm.hpp>
@@ -65,10 +66,9 @@ GameMainWindow::content(const SceneShader & shader) const
}
void
-GameMainWindow::environment(const SceneShader & s, const SceneRenderer & r) const
+GameMainWindow::environment(const SceneShader &, const SceneRenderer & r) const
{
- // default for now
- SceneProvider::environment(s, r);
+ gameState->environment->render(r, *this);
}
void