14void fields(
const Value&
value, std::initializer_list<std::string_view> names) {
15 if (!
value.isObject())
throw std::runtime_error(
"Expected sky profile object");
16 const auto keys =
value.keys();
17 if (keys.size() != names.size())
throw std::runtime_error(
"Missing or extra sky profile fields");
18 for (
const auto&
key : keys)
20 throw
std::runtime_error(
"Unknown sky profile field: " +
key);
24 if (!result)
throw std::runtime_error(std::string(
"Missing sky profile field: ") +
name);
28 if (!
value.isNumeric())
throw std::runtime_error(
"Sky profile requires a number");
29 const double result =
value.isInt64() ?
static_cast<double>(
value.asInt()) :
value.asDouble();
30 if (!std::isfinite(result))
throw std::runtime_error(
"Nonfinite sky profile number");
34float scalar(
const Value&
value,
const char*
name) {
36 if (std::abs(result) > std::numeric_limits<float>::max())
throw std::runtime_error(
"Sky profile float overflow");
37 const float converted =
static_cast<float>(result);
38 if (result != 0 && converted == 0)
throw std::runtime_error(
"Sky profile float underflow");
41template <
typename T,
size_t N>
42std::array<T, N> vector(
const Value&
object,
const char*
name) {
44 if (!
value.isArray() ||
value.arraySize() != N)
throw std::runtime_error(
"Invalid sky profile vector size");
45 std::array<T, N> result{};
46 for (
size_t i = 0; i < N; ++i) {
48 if (std::abs(item) > std::numeric_limits<T>::max())
throw std::runtime_error(
"Sky profile vector overflow");
49 result[i] =
static_cast<T
>(item);
50 if (item != 0 && result[i] == 0)
throw std::runtime_error(
"Sky profile vector underflow");
54void text(
const Value&
object,
const char*
name, std::string_view expected) {
56 if (!
value.isString() ||
value.asString() != expected)
57 throw std::runtime_error(std::string(
"Unsupported sky profile ") +
name);
64 std::optional<uint32_t>
seed;
66SkyProfile::SkyProfile(std::shared_ptr<const Impl> data) : data_(
std::move(data)) {}
74 const auto& version = field(document,
"version");
75 if (!version.isInt64() || version.asInt() < 1 || version.asInt() > 6)
76 throw std::runtime_error(
"Unsupported sky profile version");
77 if (version.asInt() == 1)
78 fields(document, {
"schema",
"version",
"mode",
"clock",
"weather",
"sun",
"atmosphere"});
79 else if (version.asInt() == 2)
80 fields(document, {
"schema",
"version",
"mode",
"clock",
"weather",
"sun",
"atmosphere",
"heightFog"});
81 else if (version.asInt() == 3)
83 {
"schema",
"version",
"mode",
"clock",
"weather",
"sun",
"atmosphere",
"heightFog",
"wispsAsset"});
84 else if (version.asInt() == 6)
85 fields(document, {
"schema",
"version",
"mode",
"clock",
"weather",
"sun",
"atmosphere",
"heightFog",
86 "wispsAsset",
"cloudMotion",
"proceduralSeed"});
88 fields(document, {
"schema",
"version",
"mode",
"clock",
"weather",
"sun",
"atmosphere",
"heightFog",
89 "wispsAsset",
"cloudMotion"});
90 text(document,
"schema",
"eve.sky-profile");
91 text(document,
"mode",
"atmosphere");
92 auto result = std::make_shared<Impl>();
93 if (version.asInt() >= 3) {
94 const auto&
path = field(document,
"wispsAsset");
95 if (version.asInt() == 6) {
96 const auto&
seed = field(document,
"proceduralSeed");
97 if (!
seed.isInt64() ||
seed.asInt() < 0 ||
seed.asInt() > 4294967295LL)
98 throw std::runtime_error(
"Invalid procedural sky seed");
99 if (
path.isNull()) result->seed =
static_cast<uint32_t
>(
seed.asInt());
101 if (!result->seed && (!
path.isString() ||
path.asString().empty() ||
path.asString().size() > 4096))
102 throw std::runtime_error(
"Invalid wisps manifest path");
103 if (!result->seed) result->wispsAsset =
path.asString();
105 if (version.asInt() >= 4) {
106 const auto&
source = field(document,
"cloudMotion");
107 fields(
source, {
"phase",
"speed",
"timeScale",
"windMultiplier"});
111 auto motionValid = result->settings.cloudMotion.validate();
113 const auto& clock = field(document,
"clock");
114 fields(clock, {
"initialHour",
"hoursPerSecond"});
115 result->settings.clock = {
number(clock,
"initialHour"),
number(clock,
"hoursPerSecond")};
116 const auto& weather = field(document,
"weather");
117 fields(weather, {
"cloudCoverage",
"fog",
"rain",
"snow",
"windXZ"});
118 result->settings.weather = {
number(weather,
"cloudCoverage"),
number(weather,
"fog"),
number(weather,
"rain"),
119 number(weather,
"snow"), vector<double, 2>(weather,
"windXZ")};
120 const auto& sun = field(document,
"sun");
121 fields(sun, {
"pitchDegrees",
"yawDegrees",
"irradiance"});
122 result->settings.sunPitchDegrees =
number(sun,
"pitchDegrees");
123 result->settings.sunYawDegrees =
number(sun,
"yawDegrees");
124 result->settings.sunIrradiance = vector<float, 3>(sun,
"irradiance");
125 const auto&
atmosphere = field(document,
"atmosphere");
126 fields(
atmosphere, {
"rayleigh",
"rayleighHeightKm",
"mieScattering",
"mieHeightKm",
"mieAbsorption",
127 "mieAnisotropy",
"ozoneAbsorption",
"ozoneCenterKm",
"ozoneHalfWidthKm",
"groundRadiusKm",
128 "atmosphereHeightKm",
"groundAlbedo",
"multiScatteringFactor"});
129 auto&
p = result->atmosphere;
130 p.rayleigh = vector<float, 3>(
atmosphere,
"rayleigh");
131 p.rayleighHeightKm = scalar(
atmosphere,
"rayleighHeightKm");
132 p.mieScattering = vector<float, 3>(
atmosphere,
"mieScattering");
134 p.mieAbsorption = vector<float, 3>(
atmosphere,
"mieAbsorption");
135 p.mieAnisotropy = scalar(
atmosphere,
"mieAnisotropy");
136 p.ozoneAbsorption = vector<float, 3>(
atmosphere,
"ozoneAbsorption");
137 p.ozoneCenterKm = scalar(
atmosphere,
"ozoneCenterKm");
138 p.ozoneHalfWidthKm = scalar(
atmosphere,
"ozoneHalfWidthKm");
139 p.groundRadiusKm = scalar(
atmosphere,
"groundRadiusKm");
140 p.atmosphereHeightKm = scalar(
atmosphere,
"atmosphereHeightKm");
141 p.groundAlbedo = vector<float, 3>(
atmosphere,
"groundAlbedo");
142 p.multiScatteringFactor = scalar(
atmosphere,
"multiScatteringFactor");
143 if (version.asInt() >= 2) {
144 const auto&
source = field(document,
"heightFog");
145 if (version.asInt() >= 5)
146 fields(
source, {
"densityPerMetre",
"heightFalloffPerMetre",
"baseHeightMetres",
"startDistanceMetres",
147 "inscattering",
"maximumOpacity",
"directionalInscattering",
"directionalExponent",
148 "directionalStartDistanceMetres",
"atmosphereContribution",
"skyDistanceMetres",
149 "directionalElevationRange"});
151 fields(
source, {
"densityPerMetre",
"heightFalloffPerMetre",
"baseHeightMetres",
"startDistanceMetres",
152 "inscattering",
"maximumOpacity",
"directionalInscattering",
"directionalExponent",
153 "directionalStartDistanceMetres",
"atmosphereContribution",
"skyDistanceMetres"});
154 auto& fog =
p.heightFog;
155 if (version.asInt() >= 5)
156 fog.directionalElevationRange = vector<float, 2>(
source,
"directionalElevationRange");
157 fog.densityPerMetre = scalar(
source,
"densityPerMetre");
158 fog.heightFalloffPerMetre = scalar(
source,
"heightFalloffPerMetre");
159 fog.baseHeightMetres = scalar(
source,
"baseHeightMetres");
160 fog.startDistanceMetres = scalar(
source,
"startDistanceMetres");
161 fog.inscattering = vector<float, 3>(
source,
"inscattering");
162 fog.maximumOpacity = scalar(
source,
"maximumOpacity");
163 fog.directionalInscattering = vector<float, 3>(
source,
"directionalInscattering");
164 fog.directionalExponent = scalar(
source,
"directionalExponent");
165 fog.directionalStartDistanceMetres = scalar(
source,
"directionalStartDistanceMetres");
166 fog.atmosphereContribution = scalar(
source,
"atmosphereContribution");
167 fog.skyDistanceMetres = scalar(
source,
"skyDistanceMetres");
171 auto orbit =
SkySolarOrbit::create(result->settings.sunPitchDegrees, result->settings.sunYawDegrees);
173 for (
float intensity : result->settings.sunIrradiance)
174 if (intensity < 0)
throw std::runtime_error(
"Solar irradiance must be nonnegative");
175 auto optical =
p.validate();
178 }
catch (
const std::exception&
error) {
std::vector< std::string > fields
const UnitySourceAsset & source
Owning, renderer-independent dynamic values.
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
The canonical owning dynamic value used by data-facing protocols.
Immutable validated atmospheric sky profile, schema eve.sky-profile versions 1 through 6.
static Result< SkyProfile > decode(const Value &document)
Validate a complete profile without creating GPU resources or changing any runtime.
const SkyRuntimeSettings & settings() const noexcept
Borrow runtime settings, valid for this profile's lifetime.
const graphics::SkyAtmosphereParameters & atmosphere() const noexcept
Borrow optical settings, valid for this profile's lifetime.
const std::string & wispsAsset() const noexcept
Borrow optional VFS wisps manifest path; empty for versions 1/2 or explicitly selected procedural ass...
std::optional< uint32_t > proceduralSeed() const noexcept
Explicit procedural-v1 seed when version 6 selects a null asset; absent for loaded/legacy profiles.
static Result< SkySolarOrbit > create(double pitchDegrees, double yawDegrees)
Prepare a daily orbit with pitch [-90,90] and yaw [-360,360] degrees.
static Result< SkyTimeline > create(const SkyClock &clock, const SkyWeather &weather)
Construct validated state. Clock hours are [0,24), speed [0,24] hours/second.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
const EditorValue * field(const EditorValue &value, const char *name)
std::optional< uint32_t > seed
SkyRuntimeSettings settings
graphics::SkyAtmosphereParameters atmosphere
Independent runtime settings; not a serialized profile or a legacy DayNight override.
Spherical atmospheric coefficients in inverse kilometres and kilometre heights.