73 const auto manifest = document(manifestPath, 65536);
74 const auto schema =
text(manifest,
"schema");
75 const bool lunar = schema ==
"eve.sky-wisps/7";
76 const bool packed = lunar || schema ==
"eve.sky-wisps/6";
79 {
"schema",
"mesh",
"density",
"material",
"contrast",
"sunDisk",
"sunDiskCurve",
"daylight",
"stars",
80 "starsNoise",
"optics",
"moonDisk",
"moonColorTexture",
"moonNormalTexture"});
81 else if (schema ==
"eve.sky-wisps/1")
82 fields(manifest, {
"schema",
"mesh",
"density",
"material"});
83 else if (schema ==
"eve.sky-wisps/2")
84 fields(manifest, {
"schema",
"mesh",
"density",
"material",
"contrast"});
85 else if (schema ==
"eve.sky-wisps/3")
86 fields(manifest, {
"schema",
"mesh",
"density",
"material",
"contrast",
"sunDisk"});
87 else if (schema ==
"eve.sky-wisps/4")
88 fields(manifest, {
"schema",
"mesh",
"density",
"material",
"contrast",
"sunDisk",
"sunDiskCurve"});
89 else if (schema ==
"eve.sky-wisps/5")
90 fields(manifest, {
"schema",
"mesh",
"density",
"material",
"contrast",
"sunDisk",
"sunDiskCurve",
91 "daylight",
"stars",
"starsNoise"});
92 else if (schema ==
"eve.sky-wisps/6")
93 fields(manifest, {
"schema",
"mesh",
"density",
"material",
"contrast",
"sunDisk",
"sunDiskCurve",
94 "daylight",
"stars",
"starsNoise",
"optics"});
96 throw std::runtime_error(
"Unsupported wisps schema");
97 auto result = std::make_shared<Impl>();
98 const auto mesh = document(resourcePath(manifestPath,
text(manifest,
"mesh")), 32 * 1024 * 1024);
99 fields(
mesh, {
"schema",
"space",
"uvChannel",
"layout",
"topology",
"vertices",
"sourceSha256"});
100 if (
text(
mesh,
"schema") !=
"eve.sky-mesh/1" ||
text(
mesh,
"space") !=
"unreal-local-centimetres" ||
101 text(
mesh,
"topology") !=
"triangle-list" || !field(
mesh,
"uvChannel").isInt64() ||
102 field(
mesh,
"uvChannel").asInt() != 3)
103 throw std::runtime_error(
"Unsupported wisps mesh contract");
105 if (
hash.size() != 64 ||
hash.find_first_not_of(
"0123456789abcdef") != std::string::npos)
106 throw std::runtime_error(
"Invalid source provenance hash");
108 constexpr const char* names[]{
"x",
"y",
"z",
"u",
"v",
"r",
"g",
"b",
"a"};
109 if (!
layout.isArray() ||
layout.arraySize() != 9)
throw std::runtime_error(
"Invalid mesh layout");
110 for (
size_t i = 0; i < 9; ++i)
111 if (!
layout.at(i).isString() ||
layout.at(i).asString() != names[i])
112 throw std::runtime_error(
"Unsupported mesh layout");
116 throw std::runtime_error(
"Invalid sky mesh corner count");
117 result->corners.reserve(
vertices.arraySize() * 9);
118 for (
size_t i = 0; i <
vertices.arraySize(); ++i) {
119 const auto corner = vector<9>(
vertices.at(i));
120 result->corners.insert(result->corners.end(), corner.begin(), corner.end());
122 const auto& density = field(manifest,
"density");
123 fields(density, {
"path",
"width",
"height",
"encoding"});
124 const auto dimension = [&](
const char*
name) {
125 const auto&
n = field(density,
name);
126 if (!
n.isInt64() ||
n.asInt() < 1 ||
n.asInt() > 4096)
127 throw std::runtime_error(
"Invalid wisps texture dimensions");
128 return static_cast<uint32_t
>(
n.asInt());
130 auto&
image = result->texture;
131 image.width = dimension(
"width");
132 image.height = dimension(
"height");
135 throw std::runtime_error(
"Unsupported wisps density encoding");
136 auto pixels =
bytes(resourcePath(manifestPath,
text(density,
"path")), 16 * 1024 * 1024);
138 throw std::runtime_error(
"Truncated wisps density texture");
139 const auto*
source =
static_cast<const std::byte*
>(
pixels->getData());
141 image.format = ShaderImageFormat::RGBA8Srgb;
142 result->pixels.resize(
pixels->getSize() * 4);
143 for (
size_t i = 0; i <
pixels->getSize(); ++i) {
144 result->pixels[i * 4] = result->pixels[i * 4 + 1] = result->pixels[i * 4 + 2] =
source[i];
145 result->pixels[i * 4 + 3] = std::byte{255};
148 image.format = ShaderImageFormat::R8;
151 image.bytes = result->pixels;
152 image.sampler = TextureSampler::linear();
153 image.sampler.repeatU =
image.sampler.repeatV =
true;
154 const auto&
material = field(manifest,
"material");
155 fields(
material, {
"domeScale",
"color",
"gradient",
"movement",
"moonForward",
"morphRate",
"morphPeriod",
156 "morphAmount",
"opacity",
"litIntensity",
"cloudTime",
"moonGradient",
"staticClouds"});
157 auto&
w = result->layer;
158 if (schema !=
"eve.sky-wisps/1")
w.materialContrast = vector<3>(field(manifest,
"contrast"));
159 w.authoredSkyLighting = schema !=
"eve.sky-wisps/1";
160 if (packed || schema ==
"eve.sky-wisps/3" || schema ==
"eve.sky-wisps/4" || schema ==
"eve.sky-wisps/5") {
161 const auto& disk = field(manifest,
"sunDisk");
162 fields(disk, {
"color",
"shape"});
163 w.sunDiskColor = vector<3>(field(disk,
"color"));
164 w.sunDiskShape = vector<3>(field(disk,
"shape"));
165 if (*std::min_element(
w.sunDiskColor.begin(),
w.sunDiskColor.end()) < 0 ||
w.sunDiskShape[0] <= 0 ||
166 w.sunDiskShape[0] > 3.141593f ||
w.sunDiskShape[1] <= 0 ||
w.sunDiskShape[2] < 0)
167 throw std::runtime_error(
"Invalid solar disk radiance or shape");
169 if (packed || schema ==
"eve.sky-wisps/4" || schema ==
"eve.sky-wisps/5") {
170 const auto&
curve = field(manifest,
"sunDiskCurve");
171 if (!
curve.isArray() ||
curve.arraySize() != 12)
172 throw std::runtime_error(
"Solar curve requires twelve RGB keys");
173 for (
size_t i = 0; i < 12; ++i) {
175 if (
key[0] < 0 ||
key[0] > 1 ||
key[1] < 0 || (i % 4 &&
key[0] <=
w.sunDiskCurve[i - 1][0]) ||
176 (i % 4 == 3 &&
key[1] <= 0))
177 throw std::runtime_error(
"Invalid solar curve key order or value");
178 w.sunDiskCurve[i] =
key;
180 w.sunDiskCurveEnabled =
true;
182 if (schema ==
"eve.sky-wisps/5" || packed) {
183 const auto& cycle = field(manifest,
"daylight");
184 fields(cycle, {
"sun",
"moon",
"moonOrbit",
"dayTint",
"duskTint",
"nightTint",
"glow",
"controls",
"stars",
185 "starUv",
"twinkle",
"scatteringCurve"});
186 auto&
d =
w.daylight;
187 d.sun = vector<4>(field(cycle,
"sun"));
188 d.moon = vector<4>(field(cycle,
"moon"));
189 d.moonOrbit = vector<4>(field(cycle,
"moonOrbit"));
190 d.dayTint = vector<4>(field(cycle,
"dayTint"));
191 d.duskTint = vector<4>(field(cycle,
"duskTint"));
192 d.nightTint = vector<4>(field(cycle,
"nightTint"));
193 d.glow = vector<4>(field(cycle,
"glow"));
194 d.controls = vector<4>(field(cycle,
"controls"));
195 d.stars = vector<4>(field(cycle,
"stars"));
196 d.starUv = vector<4>(field(cycle,
"starUv"));
197 d.twinkle = vector<4>(field(cycle,
"twinkle"));
198 const auto& keys = field(cycle,
"scatteringCurve");
199 if (!keys.isArray() || keys.arraySize() !=
d.scatteringCurve.size())
200 throw std::runtime_error(
"Scattering curve requires 21 shared angle/RGB keys");
201 for (
size_t i = 0; i <
d.scatteringCurve.size(); ++i)
d.scatteringCurve[i] = vector<4>(keys.at(i));
203 const auto& optical = field(manifest,
"optics");
204 fields(optical, {
"rayDay",
"rayDusk",
"rayNight",
"absorptionDay",
"absorptionNight"});
205 d.rayDay = vector<4>(field(optical,
"rayDay"));
206 d.rayDusk = vector<4>(field(optical,
"rayDusk"));
207 d.rayNight = vector<4>(field(optical,
"rayNight"));
208 d.absorptionDay = vector<4>(field(optical,
"absorptionDay"));
209 d.absorptionNight = vector<4>(field(optical,
"absorptionNight"));
210 w.opticalCycleEnabled =
true;
213 const auto& disk = field(manifest,
"moonDisk");
214 fields(disk, {
"color",
"shape",
"lighting",
"glow"});
215 d.moonDiskColor = vector<4>(field(disk,
"color"));
216 d.moonDiskShape = vector<4>(field(disk,
"shape"));
217 d.moonDiskLighting = vector<4>(field(disk,
"lighting"));
218 d.moonDiskGlow = vector<4>(field(disk,
"glow"));
220 auto checked =
d.validate();
221 if (!checked)
throw std::runtime_error(checked.status().describe());
222 const std::array names{
"stars",
"starsNoise",
"moonColorTexture",
"moonNormalTexture"};
223 for (
size_t i = 0; i < (lunar ? 4u : 2u); ++i) {
224 const auto&
descriptor = field(manifest, names[i]);
229 auto& star = result->celestialImages[i];
230 const auto extent = [&](
const char*
key) {
232 if (!
v.isInt64() ||
v.asInt() < 1 ||
v.asInt() > 4096)
233 throw std::runtime_error(
"Invalid star extent");
234 return static_cast<uint32_t
>(
v.asInt());
236 star.width = extent(
"width");
237 star.height = extent(
"height");
239 if (format !=
"rgba8-srgb" && format !=
"rgba8-linear" && !(packed && format ==
"rgba16-float"))
240 throw std::runtime_error(
"Invalid star encoding");
241 star.format = format ==
"rgba8-srgb" ? ShaderImageFormat::RGBA8Srgb : ShaderImageFormat::RGBA8;
242 if (format ==
"rgba16-float") star.format = ShaderImageFormat::RGBA16Float;
243 if (packed) star.mipLevels = extent(
"mipLevels");
244 unsigned maximumMips = 1;
245 for (
unsigned size = std::max(star.width, star.height);
size > 1;
size >>= 1) ++maximumMips;
246 if (star.mipLevels > maximumMips)
throw std::runtime_error(
"Invalid star mip count");
248 for (
unsigned mip = 0; mip < star.mipLevels; ++mip)
249 expected +=
size_t(std::max(1u, star.width >> mip)) * std::max(1u, star.height >> mip) *
250 (format ==
"rgba16-float" ? 8u : 4u);
252 if (data->getSize() != expected)
throw std::runtime_error(
"Truncated star texture");
253 const auto*
begin =
static_cast<const std::byte*
>(data->getData());
254 result->celestialPixels[i].assign(
begin,
begin + data->getSize());
255 star.bytes = result->celestialPixels[i];
256 star.sampler = star.mipLevels > 1 ? TextureSampler::linearMipmap() : TextureSampler::linear();
257 star.sampler.repeatU = star.sampler.repeatV = i < 2;
259 w.starsTexture = &result->celestialImages[0];
260 w.starsNoiseTexture = &result->celestialImages[1];
262 w.moonColorTexture = &result->celestialImages[2];
263 w.moonNormalTexture = &result->celestialImages[3];
264 if (
w.moonColorTexture->format != ShaderImageFormat::RGBA16Float ||
265 w.moonNormalTexture->format != ShaderImageFormat::RGBA16Float)
266 throw std::runtime_error(
"Lunar runtime samples require linear RGBA16F");
268 w.daylightEnabled =
true;
270 w.corners = result->corners;
271 w.densityTexture = &
image;
272 w.color = vector<3>(field(
material,
"color"));
273 w.gradient = vector<4>(field(
material,
"gradient"));
274 w.movement = vector<2>(field(
material,
"movement"));
275 w.moonForward = vector<3>(field(
material,
"moonForward"));
286 }
catch (
const std::exception& e) {
288 Diagnostic::error(DiagnosticCode::InvalidArgument, e.what(),
"sky.wisps-asset"));
292 const auto failure = [] {
294 "Invalid daylight material coefficients",
"sky.daylight"));
296 for (
const auto*
v : {&sun, &moon, &dayTint, &duskTint, &nightTint, &glow, &controls, &stars, &twinkle})
298 if (!std::isfinite(
x) || x < 0 || x > 1e6f)
return failure();
299 for (
const auto*
v : {&moonOrbit, &starUv})
301 if (!std::isfinite(
x) || std::abs(
x) > 1e6f)
return failure();
302 if (std::abs(moonOrbit[0]) > 90 || std::abs(moonOrbit[1]) > 360 || std::abs(moonOrbit[2]) >= 1 ||
303 std::abs(moonOrbit[3]) > 1 || controls[2] > 1 || starUv[0] <= 0 || twinkle[3] != 0)
305 for (
size_t i = 0; i < scatteringCurve.size(); ++i) {
306 const auto& k = scatteringCurve[i];
308 if (!std::isfinite(
x) || x < 0 || x > 1e6f)
return failure();
309 if (k[0] > 180 || (i && k[0] <= scatteringCurve[i - 1][0]))
return failure();
311 for (
const auto*
v : {&rayDay, &rayDusk, &rayNight, &absorptionDay, &absorptionNight})
313 if (!std::isfinite(
x) || x < 0 || x > 1)
return failure();
314 if (rayDusk[3] != 0 || rayNight[3] != 0)
return failure();
315 for (
const auto*
v : {&moonDiskColor, &moonDiskShape, &moonDiskLighting, &moonDiskGlow})
317 if (!std::isfinite(
x) || std::abs(
x) > 1e6f)
return failure();
318 for (
size_t i = 0; i < 3; ++i)
319 if (moonDiskColor[i] < 0)
return failure();
320 if ((moonDiskColor[3] != 0 && moonDiskColor[3] != 1) || moonDiskShape[0] <= 0 || moonDiskShape[0] > 1 ||
321 moonDiskShape[1] <= 0 || moonDiskShape[1] > 10 || std::abs(moonDiskShape[2]) > 360 || moonDiskShape[3] < 0 ||
322 moonDiskShape[3] > 1 || moonDiskLighting[0] < 0 || moonDiskLighting[1] < 0 || moonDiskLighting[2] < 0 ||
323 moonDiskLighting[2] > 29.53f || moonDiskLighting[3] < 0 || moonDiskLighting[3] > 10 || moonDiskGlow[0] < 0 ||
324 moonDiskGlow[1] <= 0 || moonDiskGlow[2] != 0 || moonDiskGlow[3] != 0)