载入中...
搜索中...
未找到
EvpackVegetationScene.cpp
浏览该文件的文档.
2
5
6#include <algorithm>
7#include <cmath>
8#include <cstdio>
9#include <glm/common.hpp>
10#include <glm/geometric.hpp>
11#include <limits>
12#include <new>
13#include <numeric>
14#include <set>
15
16namespace eve::asset_graphics {
17namespace {
18const Value& member(const Value::Object& object, std::string_view name) { return object.at(std::string(name)); }
19
20bool exact(const Value::Object& object, std::initializer_list<std::string_view> fields) {
21 if (object.size() != fields.size()) return false;
22 return std::all_of(fields.begin(), fields.end(), [&](auto name) { return object.contains(std::string(name)); });
23}
24
25bool numeric(const Value& value, float& result) {
26 if (!value.isNumeric()) return false;
27 const double decoded = value.isInt64() ? double(value.asInt()) : value.asDouble();
28 if (!std::isfinite(decoded) || decoded < -std::numeric_limits<float>::max() ||
29 decoded > std::numeric_limits<float>::max())
30 return false;
31 result = float(decoded);
32 return true;
33}
34
35template <std::size_t N>
36bool values(const Value& value, std::array<float, N>& result) {
37 const auto* array = value.getIf<Value::Array>();
38 if (!array || array->size() != N) return false;
39 for (std::size_t index = 0; index < N; ++index)
40 if (!numeric((*array)[index], result[index])) return false;
41 return true;
42}
43
44bool validGuid(const Value& value, std::string& result) {
45 if (!value.isString()) return false;
46 result = value.asString();
47 return result.empty() ||
48 (result.size() == 32 && std::all_of(result.begin(), result.end(), [](unsigned char c) {
49 return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
50 }));
51}
52
53bool unit(float value) { return value >= 0 && value <= 1; }
54bool nonnegative(float value) { return value >= 0 && std::isfinite(value); }
55bool whole(float value) { return std::trunc(value) == value; }
56
57bool finite(glm::vec2 value) { return std::isfinite(value.x) && std::isfinite(value.y); }
58bool finite(glm::vec4 value) {
59 return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w);
60}
61
62float remap(float value, float minimum, float maximum, float epsilon) {
63 value = std::clamp(value, .0001f, .9999f);
64 const float denominator = maximum - minimum + epsilon;
65 if (denominator == 0.f) return value >= maximum ? 1.f : 0.f;
66 return std::clamp((value - minimum) / denominator, 0.f, 1.f);
67}
68
69glm::vec3 rotate(const std::array<float, 4>& rotation, glm::vec3 value) {
70 const glm::vec3 xyz(rotation[0], rotation[1], rotation[2]);
71 const glm::vec3 t = 2.f * glm::cross(xyz, value);
72 return value + rotation[3] * t + glm::cross(xyz, t);
73}
74
75glm::vec3 inverseRotate(const std::array<float, 4>& rotation, glm::vec3 value) {
76 const std::array<float, 4> inverse{-rotation[0], -rotation[1], -rotation[2], rotation[3]};
77 return rotate(inverse, value);
78}
79
80glm::vec4 sampleMask(const graphics::VegetationMask& mask, glm::vec2 uv) {
81 if (mask.pixels.empty()) return glm::vec4(1.f);
82 const float x = std::clamp(uv.x * mask.width - .5f, 0.f, float(mask.width - 1));
83 const float y = std::clamp(uv.y * mask.height - .5f, 0.f, float(mask.height - 1));
84 const auto x0 = std::uint32_t(x), y0 = std::uint32_t(y);
85 const auto x1 = std::min(x0 + 1, mask.width - 1), y1 = std::min(y0 + 1, mask.height - 1);
86 return glm::mix(glm::mix(mask.pixels[std::size_t(y0) * mask.width + x0],
87 mask.pixels[std::size_t(y0) * mask.width + x1], x - float(x0)),
88 glm::mix(mask.pixels[std::size_t(y1) * mask.width + x0],
89 mask.pixels[std::size_t(y1) * mask.width + x1], x - float(x0)),
90 y - float(y0));
91}
92
93glm::vec4 seasonal(const VegetationSceneElement& element, float season) {
94 if (!element.seasonal) return glm::vec4(element.value[0], element.value[1], element.value[2], element.value[3]);
95 const float wrapped = season == 4.f ? 0.f : season;
96 const auto index = std::uint32_t(wrapped);
97 const float fraction = wrapped - float(index);
98 const float smooth = fraction * fraction * (3.f - 2.f * fraction);
99 const auto& a = element.seasons[index];
100 const auto& b = element.seasons[(index + 1) % 4];
101 return glm::mix(glm::vec4(a[0], a[1], a[2], a[3]), glm::vec4(b[0], b[1], b[2], b[3]), smooth);
102}
103
104float propertyScalar(const VegetationSceneElement& element, std::string_view name, float fallback) {
105 const auto found = std::find_if(element.properties.begin(), element.properties.end(),
106 [&](const auto& property) { return property.name == name; });
107 return found == element.properties.end() ? fallback : found->value;
108}
109
110glm::vec4 propertyVector(const VegetationSceneElement& element, std::string_view name, glm::vec4 fallback) {
111 const auto found = std::find_if(element.properties.begin(), element.properties.end(),
112 [&](const auto& property) { return property.name == name; });
113 if (found == element.properties.end()) return fallback;
114 return {found->vector[0], found->vector[1], found->vector[2], found->vector[3]};
115}
116} // namespace
117
119 const AssetRef& asset, const asset::EvpackCapabilities& capabilities, std::uint64_t maximumDecodedBytes) const {
120 auto payload = reader_.read(asset, "eve.vegetation-scene/1", capabilities, maximumDecodedBytes);
122 const asset::RuntimeAssetChunk* definition = nullptr;
123 for (const auto& chunk : payload.value().chunks) {
124 if (chunk.kind != asset::EvpackChunkKind::Definition) continue;
125 if (definition)
127 DiagnosticCode::Conflict, "duplicate scene definition", {}, {}, "asset.graphics.vegetation-scene"));
128 definition = &chunk;
129 }
130 if (!definition)
132 DiagnosticCode::NotFound, "scene definition is missing", {}, {}, "asset.graphics.vegetation-scene"));
134 limits.maximumBytes = maximumDecodedBytes;
135 auto decoded = asset::decodeRuntimeDefinition(definition->bytes, limits);
136 if (!decoded) return Result<LoadedVegetationScene>::failure(decoded.status());
137 const auto* root = decoded.value().getIf<Value::Object>();
138 if (!root || !exact(*root, {"schema", "schemaVersion", "sourceGuid", "control", "details", "motion", "volume", "elements"}) ||
139 !member(*root, "schema").isString() || member(*root, "schema").asString() != "eve.vegetation-scene" ||
140 !member(*root, "schemaVersion").isInt64() || member(*root, "schemaVersion").asInt() != 1 ||
141 !member(*root, "sourceGuid").isString())
143 "vegetation scene root is malformed", {}, {},
144 "asset.graphics.vegetation-scene"));
145 std::string sourceGuid;
146 if (!validGuid(member(*root, "sourceGuid"), sourceGuid) || sourceGuid.empty())
148 "vegetation scene source GUID is invalid", {},
149 {}, "asset.graphics.vegetation-scene"));
150
151 const auto* control = member(*root, "control").getIf<Value::Object>();
152 const auto* details = member(*root, "details").getIf<Value::Object>();
153 const auto* motion = member(*root, "motion").getIf<Value::Object>();
154 const auto* volume = member(*root, "volume").getIf<Value::Object>();
155 if (!control || !details || !motion || !volume ||
156 !exact(*control, {"values", "globalColor", "overlayColor", "overlayAlbedoGuid", "overlayNormalGuid",
157 "noiseTextureGuid"}) ||
158 !exact(*details, {"layers", "global", "colorMask", "overlayMask", "alphaThreshold", "perspective",
159 "motionHighlight", "bending", "flutter", "interactionAmplitude"}) ||
160 !exact(*motion, {"values", "direction", "noiseTextureGuid"}) ||
161 !exact(*volume, {"values", "colors", "extras", "motion", "vertex"}))
163 "vegetation scene sections are malformed", {},
164 {}, "asset.graphics.vegetation-scene"));
165
166 LoadedVegetationScene result{asset, std::move(sourceGuid), payload.value().variant, {}, {}, {}, {}, {}};
167 std::array<float, 17> controls{};
168 std::array<float, 8> motions{};
169 std::array<float, 4> volumes{};
170 std::array<float, 2> direction{};
171 if (!values(member(*control, "values"), controls) || !values(member(*control, "globalColor"), result.control.globalColor) ||
172 !values(member(*control, "overlayColor"), result.control.overlayColor) ||
173 !values(member(*motion, "values"), motions) || !values(member(*motion, "direction"), direction) ||
174 !values(member(*volume, "values"), volumes) ||
175 !validGuid(member(*control, "overlayAlbedoGuid"), result.control.overlayAlbedoGuid) ||
176 !validGuid(member(*control, "overlayNormalGuid"), result.control.overlayNormalGuid) ||
177 !validGuid(member(*control, "noiseTextureGuid"), result.control.noiseTextureGuid) ||
178 !validGuid(member(*motion, "noiseTextureGuid"), result.motion.noiseTextureGuid))
180 "vegetation scene values are malformed", {}, {},
181 "asset.graphics.vegetation-scene"));
182 auto& c = result.control;
183 c.season = controls[0]; c.globalAlpha = controls[1]; c.globalOverlay = controls[2];
184 c.globalWetness = controls[3]; c.globalEmissive = controls[4]; c.globalSubsurface = controls[5];
185 c.globalSize = controls[6]; c.overlaySmoothness = controls[7]; c.overlayNormalScale = controls[8];
186 c.overlaySubsurface = controls[9]; c.overlayScale = controls[10]; c.wetnessContrast = controls[11];
187 c.wetnessNormalScale = controls[12]; c.noiseTiling = controls[13]; c.proximityFade = controls[14];
188 c.distanceFadeBias = controls[15]; c.defaultConformHeight = controls[16];
189 auto& m = result.motion;
190 m.windPower = motions[0]; m.noiseTiling = motions[1]; m.bending = motions[2]; m.branch = motions[3];
191 m.flutter = motions[4]; m.speed = motions[5]; m.animatedTime = motions[6] >= .5f; m.fadeDistance = motions[7];
192 m.direction = direction;
193 if (!whole(volumes[1]) || !whole(volumes[2]))
195 "vegetation volume policy enum is not integral",
196 {}, {}, "asset.graphics.vegetation-scene"));
197 result.volume.renderScale = volumes[0]; result.volume.edgeFade = volumes[3];
198 result.volume.visibility = std::uint32_t(volumes[1]); result.volume.sorting = std::uint32_t(volumes[2]);
199 const std::array<std::string_view, 4> channelNames{"colors", "extras", "motion", "vertex"};
200 for (std::size_t index = 0; index < channelNames.size(); ++index) {
201 std::array<float, 3> channel{};
202 if (!values(member(*volume, channelNames[index]), channel))
204 "vegetation volume channel is malformed",
205 {}, {}, "asset.graphics.vegetation-scene"));
206 if (!whole(channel[0]) || !whole(channel[1]) || !whole(channel[2]) || channel[1] < 0 || channel[2] < 0)
208 Diagnostic::error(DiagnosticCode::InvalidArgument, "vegetation volume channel integers are invalid", {},
209 {}, "asset.graphics.vegetation-scene"));
210 result.volume.channels[index] = {std::int32_t(channel[0]), std::uint32_t(channel[1]),
211 std::uint32_t(channel[2])};
212 }
213 if (c.season < 0 || c.season > 4 || !unit(c.globalAlpha) || !unit(c.globalOverlay) || !unit(c.globalWetness) ||
214 !nonnegative(c.globalEmissive) || !unit(c.globalSubsurface) || !unit(c.globalSize) ||
215 !unit(c.overlaySmoothness) || !unit(c.overlayNormalScale) || !unit(c.overlaySubsurface) || c.overlayScale <= 0 ||
216 !unit(c.wetnessContrast) || !unit(c.wetnessNormalScale) || c.noiseTiling <= 0 || !nonnegative(c.proximityFade) ||
217 !nonnegative(c.distanceFadeBias) || !unit(c.globalColor[3]) || !unit(c.overlayColor[3]) ||
218 !std::all_of(c.globalColor.begin(), c.globalColor.begin() + 3, nonnegative) ||
219 !std::all_of(c.overlayColor.begin(), c.overlayColor.begin() + 3, nonnegative) || !unit(m.windPower) ||
220 m.noiseTiling <= 0 || m.bending < 0 || m.bending > 2 || m.branch < 0 || m.branch > 2 ||
221 m.flutter < 0 || m.flutter > 2 || !nonnegative(m.speed) || !nonnegative(m.fadeDistance) ||
222 result.volume.renderScale <= 0 || !unit(result.volume.edgeFade) ||
223 (result.volume.visibility != 0 && result.volume.visibility != 10 && result.volume.visibility != 20) ||
224 (result.volume.sorting != 0 && result.volume.sorting != 10 && result.volume.sorting != 20) ||
225 std::abs(std::hypot(m.direction[0], m.direction[1]) - 1.f) > 1e-4f ||
226 !std::all_of(result.volume.channels.begin(), result.volume.channels.end(), [](const auto& channel) {
227 return (channel.renderMode == -1 || channel.renderMode == 10 || channel.renderMode == 20) &&
228 channel.width >= 32 && channel.height >= 32 && channel.width <= 16384 && channel.height <= 16384;
229 }))
231 "vegetation scene values are out of range", {},
232 {}, "asset.graphics.vegetation-scene"));
233
234 Value detailDocument = Value::object({{"schema", "eve.graphics.vegetation-details"}, {"version", 1},
235 {"layers", member(*details, "layers")}, {"global", member(*details, "global")},
236 {"colorMask", member(*details, "colorMask")}, {"overlayMask", member(*details, "overlayMask")},
237 {"alphaThreshold", member(*details, "alphaThreshold")}, {"perspective", member(*details, "perspective")},
238 {"motionHighlight", member(*details, "motionHighlight")}, {"bending", member(*details, "bending")},
239 {"flutter", member(*details, "flutter")}, {"interactionAmplitude", member(*details, "interactionAmplitude")}});
240 auto restored = graphics::restoreVegetationDetails(detailDocument);
241 if (!restored) return Result<LoadedVegetationScene>::failure(restored.status());
242 result.details = std::move(restored).takeValue();
243
244 const auto* elementValues = member(*root, "elements").getIf<Value::Array>();
245 if (!elementValues || elementValues->size() > 4096)
247 "vegetation scene element count is invalid", {},
248 {}, "asset.graphics.vegetation-scene"));
249 result.elements.reserve(elementValues->size());
250 const std::set<std::string> kinds{
251 "color-effect", "color-map", "color-noise", "color-tint", "extras-alpha", "extras-emissive",
252 "extras-overlay", "extras-wetness", "motion-advanced", "motion-interaction", "motion-wind-power",
253 "vertex-conform-model", "vertex-conform-simple", "vertex-conform-terrain", "vertex-height-offset",
254 "vertex-height", "vertex-orientation-model", "vertex-orientation-terrain", "vertex-size"};
255 for (const auto& encoded : *elementValues) {
256 const auto* element = encoded.getIf<Value::Object>();
257 if (!element || !exact(*element, {"sourceFileId", "shaderGuid", "kind", "channel", "properties", "enabled", "visibility", "position",
258 "rotation", "scale", "layers", "intensity", "value", "seasonal",
259 "seasons", "textureGuid", "textureAsset", "remap", "blendRgb", "blendAlpha", "directionMode",
260 "invertDirection", "volumeFade", "motionMode", "motionPower"}))
262 "vegetation scene element is malformed", {},
263 {}, "asset.graphics.vegetation-scene"));
264 VegetationSceneElement decodedElement;
265 const auto& sourceFileId = member(*element, "sourceFileId");
266 const auto& shaderGuid = member(*element, "shaderGuid");
267 const auto& kind = member(*element, "kind");
268 const auto& channel = member(*element, "channel");
269 const auto& enabled = member(*element, "enabled");
270 const auto& visibility = member(*element, "visibility");
271 const auto& layers = member(*element, "layers");
272 const auto& seasonal = member(*element, "seasonal");
273 const auto& blendRgb = member(*element, "blendRgb");
274 const auto& blendAlpha = member(*element, "blendAlpha");
275 const auto& directionMode = member(*element, "directionMode");
276 const auto& invertDirection = member(*element, "invertDirection");
277 const auto& volumeFade = member(*element, "volumeFade");
278 const auto& motionMode = member(*element, "motionMode");
279 if (!sourceFileId.isInt64() || sourceFileId.asInt() == 0 || !kind.isString() || !kinds.contains(kind.asString()) ||
280 !channel.isInt64() || !enabled.isBool() || !visibility.isInt64() || !layers.isInt64() ||
281 !seasonal.isBool() || !blendRgb.isInt64() || !blendAlpha.isInt64() || !directionMode.isInt64() || !invertDirection.isBool() ||
282 !volumeFade.isBool() || !motionMode.isInt64() || !validGuid(shaderGuid, decodedElement.shaderGuid) ||
283 decodedElement.shaderGuid.empty() ||
284 !values(member(*element, "position"), decodedElement.position) ||
285 !values(member(*element, "rotation"), decodedElement.rotation) ||
286 !values(member(*element, "scale"), decodedElement.scale) ||
287 !values(member(*element, "value"), decodedElement.value) ||
288 !values(member(*element, "remap"), decodedElement.remap) ||
289 !numeric(member(*element, "intensity"), decodedElement.intensity) ||
290 !numeric(member(*element, "motionPower"), decodedElement.motionPower) ||
291 !validGuid(member(*element, "textureGuid"), decodedElement.textureGuid) ||
292 !member(*element, "textureAsset").isString())
294 Diagnostic::error(DiagnosticCode::ParseError, "vegetation scene element values are malformed", {}, {},
295 "asset.graphics.vegetation-scene"));
296 decodedElement.textureAsset = member(*element, "textureAsset").asString();
297 if (!decodedElement.textureAsset.empty() && !AssetRef::parse(decodedElement.textureAsset))
299 Diagnostic::error(DiagnosticCode::ParseError, "vegetation scene element texture asset is invalid", {},
300 {}, "asset.graphics.vegetation-scene"));
301 const auto* propertyValues = member(*element, "properties").getIf<Value::Array>();
302 if (!propertyValues || propertyValues->size() > 256)
304 Diagnostic::error(DiagnosticCode::InvalidArgument, "vegetation scene element property count is invalid",
305 {}, {}, "asset.graphics.vegetation-scene"));
306 decodedElement.properties.reserve(propertyValues->size());
307 std::set<std::string> propertyNames;
308 for (const auto& encodedProperty : *propertyValues) {
309 const auto* property = encodedProperty.getIf<Value::Object>();
310 VegetationSceneElementProperty decodedProperty;
311 if (!property || !exact(*property, {"name", "type", "textureGuid", "textureAsset", "vector", "value"}) ||
312 !member(*property, "name").isString() || member(*property, "name").asString().empty() ||
313 member(*property, "name").asString().size() > 128 || !member(*property, "type").isInt64() ||
314 member(*property, "type").asInt() < 0 || member(*property, "type").asInt() > 2 ||
315 !validGuid(member(*property, "textureGuid"), decodedProperty.textureGuid) ||
316 !member(*property, "textureAsset").isString() ||
317 !values(member(*property, "vector"), decodedProperty.vector) ||
318 !numeric(member(*property, "value"), decodedProperty.value) ||
319 !propertyNames.emplace(member(*property, "name").asString()).second)
321 Diagnostic::error(DiagnosticCode::ParseError, "vegetation scene element property is malformed", {},
322 {}, "asset.graphics.vegetation-scene"));
323 decodedProperty.name = member(*property, "name").asString();
324 decodedProperty.type = std::int32_t(member(*property, "type").asInt());
325 decodedProperty.textureAsset = member(*property, "textureAsset").asString();
326 if (!decodedProperty.textureAsset.empty() && !AssetRef::parse(decodedProperty.textureAsset))
328 DiagnosticCode::ParseError, "vegetation scene element property texture asset is invalid", {}, {},
329 "asset.graphics.vegetation-scene"));
330 decodedElement.properties.push_back(std::move(decodedProperty));
331 }
332 const auto* seasonValues = member(*element, "seasons").getIf<Value::Array>();
333 if (!seasonValues || seasonValues->size() != 4)
335 "vegetation scene seasons are malformed",
336 {}, {}, "asset.graphics.vegetation-scene"));
337 for (std::size_t index = 0; index < 4; ++index)
338 if (!values((*seasonValues)[index], decodedElement.seasons[index]))
340 Diagnostic::error(DiagnosticCode::ParseError, "vegetation scene season is malformed", {}, {},
341 "asset.graphics.vegetation-scene"));
342 const auto rawChannel = channel.asInt(), rawVisibility = visibility.asInt(), rawLayers = layers.asInt();
343 const auto rawBlendRgb = blendRgb.asInt(), rawBlendAlpha = blendAlpha.asInt();
344 const auto rawDirectionMode = directionMode.asInt(), rawMotionMode = motionMode.asInt();
345 if (rawChannel < 0 || rawChannel > 3 || rawVisibility < -1 || rawVisibility > 20 || rawLayers < 1 ||
346 rawLayers > 0x1ff || rawBlendRgb < 0 || rawBlendRgb > 2 || rawBlendAlpha < 0 || rawBlendAlpha > 1 ||
347 rawDirectionMode < 10 || rawDirectionMode > 40 ||
348 rawMotionMode < 13 || rawMotionMode > 15)
350 Diagnostic::error(DiagnosticCode::InvalidArgument, "vegetation scene element integers are out of range",
351 {}, {}, "asset.graphics.vegetation-scene"));
352 decodedElement.sourceFileId = sourceFileId.asInt(); decodedElement.kind = kind.asString();
353 decodedElement.channel = std::uint32_t(rawChannel); decodedElement.enabled = enabled.asBool();
354 decodedElement.visibility = std::int32_t(rawVisibility); decodedElement.layers = std::uint16_t(rawLayers);
355 decodedElement.seasonal = seasonal.asBool(); decodedElement.blendRgb = std::int32_t(rawBlendRgb);
356 decodedElement.blendAlpha = std::int32_t(rawBlendAlpha);
357 decodedElement.directionMode = std::int32_t(directionMode.asInt());
358 decodedElement.invertDirection = invertDirection.asBool(); decodedElement.volumeFade = volumeFade.asBool();
359 decodedElement.motionMode = std::int32_t(motionMode.asInt());
360 const float rotationLength = std::sqrt(std::inner_product(decodedElement.rotation.begin(), decodedElement.rotation.end(),
361 decodedElement.rotation.begin(), 0.f));
362 const std::uint32_t expectedChannel = decodedElement.kind.starts_with("color-") ? 0u :
363 decodedElement.kind.starts_with("extras-") ? 1u : decodedElement.kind.starts_with("motion-") ? 2u : 3u;
364 if (decodedElement.channel > 3 || decodedElement.channel != expectedChannel ||
365 (decodedElement.visibility != -1 && decodedElement.visibility != 0 && decodedElement.visibility != 10 &&
366 decodedElement.visibility != 20) || decodedElement.layers == 0 || (decodedElement.layers & ~0x1ffu) ||
367 !unit(decodedElement.intensity) || !unit(decodedElement.motionPower) ||
368 std::abs(rotationLength - 1.f) > 1e-3f ||
369 std::any_of(decodedElement.scale.begin(), decodedElement.scale.end(), [](float value) { return value <= 0; }) ||
370 decodedElement.remap[0] > decodedElement.remap[1] || decodedElement.remap[2] > decodedElement.remap[3] ||
371 decodedElement.remap[4] > decodedElement.remap[5] ||
372 (decodedElement.blendRgb < 0 || decodedElement.blendRgb > 2) ||
373 (decodedElement.blendAlpha < 0 || decodedElement.blendAlpha > 1) ||
374 (decodedElement.directionMode != 10 && decodedElement.directionMode != 20 &&
375 decodedElement.directionMode != 30 && decodedElement.directionMode != 40) ||
376 (decodedElement.motionMode != 13 && decodedElement.motionMode != 15))
378 "vegetation scene element is out of range",
379 {}, {}, "asset.graphics.vegetation-scene"));
380 result.elements.push_back(std::move(decodedElement));
381 }
382 return Result<LoadedVegetationScene>::success(std::move(result));
383}
384
386 const graphics::VegetationMotion& baseMotion,
387 const LoadedVegetationScene& scene) {
388 auto detailed = graphics::configureVegetationDetails(baseSurface, baseMotion, scene.details);
389 if (!detailed) return Result<VegetationSceneProjection>::failure(detailed.status());
390 VegetationSceneProjection result{std::move(detailed.value().surface), std::move(detailed.value().motion),
391 scene.volume, scene.control.season};
392 const auto& c = scene.control;
393 auto& s = result.surface;
394 s.vegetationColor.fieldColor = c.globalColor;
395 s.vegetationColor.overlay *= c.globalOverlay;
396 s.vegetationColor.wetness *= c.globalWetness;
397 s.vegetationColor.overlayColor = {c.overlayColor[0], c.overlayColor[1], c.overlayColor[2]};
398 s.vegetationColor.overlaySmoothness = c.overlaySmoothness;
399 s.vegetationColor.overlayNormalScale = c.overlayNormalScale;
400 s.vegetationColor.overlaySubsurface = c.overlaySubsurface;
401 s.vegetationColor.wetnessContrast = c.wetnessContrast;
402 s.vegetationColor.wetnessNormalScale = c.wetnessNormalScale;
403 s.vegetationAlpha.enabled = true;
404 s.vegetationAlpha.global *= c.globalAlpha;
405 s.vegetationAlpha.cameraFadeMin = (c.proximityFade + .01f) * .5f;
406 s.vegetationAlpha.cameraFadeMax = c.proximityFade + .01f;
407 if (s.vegetationEmission.enabled) s.vegetationEmission.global *= c.globalEmissive;
408 s.translucency.globalIntensity *= c.globalSubsurface;
409 s.vegetationVertex.globalSize *= c.globalSize;
410 s.vegetationVertex.distanceFadeBias *= c.distanceFadeBias + .01f;
411 auto& gpu = s.vegetationMotion;
412 gpu.fallback[2] = scene.motion.windPower;
413 gpu.globalDirection = scene.motion.direction;
414 gpu.globalBending *= scene.motion.bending;
415 gpu.globalBranch *= scene.motion.branch;
416 gpu.globalFlutter *= scene.motion.flutter;
417 gpu.noiseTiling *= scene.motion.noiseTiling;
418 gpu.fadeDistance = scene.motion.fadeDistance;
419 auto& cpu = result.motion;
420 cpu.globalBending *= scene.motion.bending;
421 cpu.globalBranch *= scene.motion.branch;
422 cpu.globalFlutter *= scene.motion.flutter;
423 cpu.noiseTiling *= scene.motion.noiseTiling;
424 cpu.fadeDistance = scene.motion.fadeDistance;
425 cpu.timeScale *= scene.motion.speed;
426 cpu.globalSize *= c.globalSize;
427 cpu.distanceFadeBias *= c.distanceFadeBias + .01f;
430 return Result<VegetationSceneProjection>::success(std::move(result));
431}
432
434 const asset::EvpackResourceReader& reader, const LoadedVegetationScene& scene,
435 const asset::EvpackCapabilities& capabilities, std::uint64_t maximumDecodedBytes) {
436 try {
437 std::map<std::string, std::string> required;
438 auto admit = [&](const std::string& guid, const std::string& asset) -> Result<void> {
439 if (guid.empty()) return Result<void>::success();
440 if (asset.empty())
442 "vegetation element texture dependency is unavailable", guid,
443 {}, "asset.graphics.vegetation-scene"));
444 const auto [position, inserted] = required.emplace(guid, asset);
445 if (!inserted && position->second != asset)
447 "vegetation element texture GUID has conflicting assets",
448 guid, {}, "asset.graphics.vegetation-scene"));
449 return Result<void>::success();
450 };
451 for (const auto& element : scene.elements) {
452 auto admitted = admit(element.textureGuid, element.textureAsset);
453 if (!admitted) return Result<std::map<std::string, graphics::VegetationMask>>::failure(admitted.status());
454 for (const auto& property : element.properties) {
455 if (property.type != 0 || (property.name != "_MainTex" && property.name != "_NoiseTex")) continue;
456 admitted = admit(property.textureGuid, property.textureAsset);
457 if (!admitted) return Result<std::map<std::string, graphics::VegetationMask>>::failure(admitted.status());
458 }
459 }
460 std::map<std::string, graphics::VegetationMask> output;
461 std::uint64_t usedBytes = 0;
462 for (const auto& [guid, encodedAsset] : required) {
463 if (usedBytes >= maximumDecodedBytes)
465 DiagnosticCode::InvalidArgument, "vegetation element masks exceed aggregate decoded budget", {}, {},
466 "asset.graphics.vegetation-scene"));
467 auto image = AssetRef::parse(encodedAsset);
471 limits.maximumPixels = 4 * 1024 * 1024;
472 limits.maximumDecodedBytes = maximumDecodedBytes - usedBytes;
473 auto decoded = asset::decodeEvpackImage(reader, image.value(), capabilities, limits);
474 if (!decoded) return Result<std::map<std::string, graphics::VegetationMask>>::failure(decoded.status());
475 const std::uint64_t pixelCount = std::uint64_t(decoded.value().width) * decoded.value().height;
476 const std::uint64_t decodedBytes = decoded.value().pixels.size();
477 const std::uint64_t maskBytes = pixelCount * sizeof(glm::vec4);
478 if (decodedBytes > maximumDecodedBytes - usedBytes ||
479 maskBytes > maximumDecodedBytes - usedBytes - decodedBytes)
481 DiagnosticCode::InvalidArgument, "vegetation element masks exceed aggregate decoded budget", {}, {},
482 "asset.graphics.vegetation-scene"));
484 mask.width = decoded.value().width;
485 mask.height = decoded.value().height;
486 mask.pixels.resize(std::size_t(pixelCount));
487 for (std::size_t index = 0; index < mask.pixels.size(); ++index) {
488 const auto offset = index * 4;
489 mask.pixels[index] = {decoded.value().pixels[offset] / 255.f, decoded.value().pixels[offset + 1] / 255.f,
490 decoded.value().pixels[offset + 2] / 255.f,
491 decoded.value().pixels[offset + 3] / 255.f};
492 }
493 usedBytes += decodedBytes + maskBytes;
494 output.emplace(guid, std::move(mask));
495 }
497 } catch (const std::bad_alloc&) {
499 Diagnostic::error(DiagnosticCode::Failed, "vegetation element mask allocation failed", {}, {},
500 "asset.graphics.vegetation-scene"));
501 }
502}
503
506 if (!finite(input.localUv) || !finite(input.mainSample) || !finite(input.elementParams) ||
507 !finite(input.vertexColor) || !std::isfinite(input.worldPosition.x) || !std::isfinite(input.worldPosition.y) ||
508 !std::isfinite(input.worldPosition.z) || !std::isfinite(input.worldNormal.x) || !std::isfinite(input.worldNormal.y) ||
509 !std::isfinite(input.worldNormal.z) || !finite(input.velocityDirection) || !finite(input.noiseSample) ||
510 !std::isfinite(input.terrainHeight) ||
511 !unit(input.volumeFade) || !std::isfinite(input.season) || input.season < 0.f ||
512 input.season > 4.f || !unit(element.intensity) || element.blendRgb < 0 || element.blendRgb > 2 ||
513 element.blendAlpha < 0 || element.blendAlpha > 1)
515 Diagnostic::error(DiagnosticCode::InvalidArgument, "vegetation element pixel inputs are invalid", {}, {},
516 "asset.graphics.vegetation-scene"));
517
519 output.blendRgb = element.blendRgb;
520 output.blendAlpha = element.blendAlpha;
522
523 const glm::vec3 remappedRgb(
524 remap(input.mainSample.r, element.remap[0], element.remap[1], 0.f),
525 remap(input.mainSample.g, element.remap[0], element.remap[1], 0.f),
526 remap(input.mainSample.b, element.remap[0], element.remap[1], 0.f));
527 const float remappedAlpha = remap(input.mainSample.a, element.remap[2], element.remap[3], .0001f);
528 const glm::vec2 centered = input.localUv * 2.f - 1.f;
529 const float radial = std::clamp(std::clamp(1.f - glm::length(centered), 0.f, 1.f), .0001f, .9999f);
530 const float falloff = remap(radial, element.remap[4], element.remap[5], .0001f);
531 const float volume = element.volumeFade ? input.volumeFade : 1.f;
532 const float alpha = element.intensity * remappedAlpha * input.elementParams.a * input.vertexColor.a * falloff * volume;
533 const glm::vec4 selected = seasonal(element, input.season);
534 const float scalar = selected.x * remappedRgb.x * input.elementParams.x * input.vertexColor.r;
535 const float alphaBlend = element.blendAlpha == 0 ? glm::mix(1.f, scalar, alpha) : scalar * alpha;
536
537 if (element.kind == "color-effect") {
538 output.value = {0.f, 0.f, 0.f, alphaBlend};
539 output.colorMask = 1;
541 output.alphaOperation = element.blendAlpha == 0 ? VegetationScenePixelBlend::Multiply
543 } else if (element.kind == "color-map") {
544 output.value = {selected.r * remappedRgb.r, selected.g * remappedRgb.g, selected.b * remappedRgb.b,
545 selected.a * alpha};
546 } else if (element.kind == "color-noise") {
547 const float minimum = propertyScalar(element, "_NoiseMinValue", 0.f);
548 const float maximum = propertyScalar(element, "_NoiseMaxValue", 1.f);
549 const float noise = maximum == minimum ? (input.noiseSample.r >= maximum ? 1.f : 0.f)
550 : std::clamp((input.noiseSample.r - minimum) / (maximum - minimum), 0.f, 1.f);
551 const glm::vec4 one = propertyVector(element, "_NoiseColorOne", glm::vec4(1.f));
552 const glm::vec4 two = propertyVector(element, "_NoiseColorTwo", glm::vec4(1.f));
553 const glm::vec4 color = glm::mix(one, two, noise);
554 output.value = {color.r, color.g, color.b, color.a * alpha * selected.a};
555 } else if (element.kind == "color-tint") {
556 output.value = {selected.r * remappedRgb.r * input.elementParams.r * input.vertexColor.r,
557 selected.g * remappedRgb.g * input.elementParams.g * input.vertexColor.g,
558 selected.b * remappedRgb.b * input.elementParams.b * input.vertexColor.b,
559 selected.a * alpha};
560 output.alphaOperation = VegetationScenePixelBlend::Add;
561 } else if (element.kind == "extras-alpha") {
562 output.value = {scalar, 0.f, 0.f, alphaBlend};
563 output.colorMask = 1;
565 output.alphaOperation = element.blendAlpha == 0 ? VegetationScenePixelBlend::Multiply
567 } else if (element.kind == "extras-emissive") {
568 output.value = {scalar, 0.f, 0.f, alpha};
569 output.colorMask = 8;
570 } else if (element.kind == "extras-overlay") {
571 output.value = {0.f, 0.f, scalar, alpha};
572 output.colorMask = 2;
573 } else if (element.kind == "extras-wetness") {
574 output.value = {0.f, scalar, 0.f, alpha};
575 output.colorMask = 4;
576 } else if (element.kind == "motion-wind-power") {
577 output.value = {0.f, 0.f, scalar, alpha};
578 output.colorMask = 2;
579 } else if (element.kind == "motion-advanced") {
580 const glm::vec3 forward3 = rotate(element.rotation, glm::vec3(0.f, 0.f, 1.f));
581 const glm::vec3 texture3 = rotate(
582 element.rotation, glm::vec3(remappedRgb.r * 2.f - 1.f, 0.f, remappedRgb.g * 2.f - 1.f));
583 glm::vec2 direction;
584 switch (element.directionMode) {
585 case 10: direction = {forward3.x, forward3.z}; break;
586 case 20: direction = {texture3.x, texture3.z}; break;
587 case 30: direction = glm::vec2(input.vertexColor) * 2.f - 1.f; break;
588 case 40: direction = input.velocityDirection; break;
589 default:
591 Diagnostic::error(DiagnosticCode::InvalidArgument, "TVE advanced motion direction mode is invalid",
592 {}, {}, "asset.graphics.vegetation-scene"));
593 }
594 if (element.invertDirection) direction = -direction;
595 const float noiseMinimum = propertyScalar(element, "_NoiseMinValue", 0.f);
596 const float noiseMaximum = propertyScalar(element, "_NoiseMaxValue", 1.f);
597 const float noiseDenominator = noiseMaximum - noiseMinimum;
598 glm::vec2 noise = noiseDenominator == 0.f
599 ? glm::step(glm::vec2(noiseMaximum), glm::vec2(input.noiseSample))
600 : glm::clamp((glm::vec2(input.noiseSample) - noiseMinimum) / noiseDenominator,
601 glm::vec2(0.f), glm::vec2(1.f));
602 const float noiseIntensity = propertyScalar(element, "_NoiseIntensityValue", 0.f);
603 const glm::vec2 encoded = glm::clamp(glm::mix(direction * .5f + .5f, noise, noiseIntensity),
604 glm::vec2(0.f), glm::vec2(1.f));
605 output.value = {encoded.x, encoded.y, element.motionPower,
606 element.intensity * remappedAlpha * input.elementParams.a * input.vertexColor.a * volume};
607 output.colorMask = std::uint8_t(element.motionMode);
608 output.alphaOperation = VegetationScenePixelBlend::Add;
609 } else if (element.kind == "vertex-size") {
610 output.value = {scalar, 0.f, 0.f, alphaBlend};
611 output.colorMask = 1;
613 output.alphaOperation = element.blendAlpha == 0 ? VegetationScenePixelBlend::Multiply
615 } else if (element.kind == "motion-interaction") {
616 const glm::vec3 localDirection(remappedRgb.r * 2.f - 1.f, 0.f, remappedRgb.g * 2.f - 1.f);
617 glm::vec3 worldDirection = rotate(element.rotation, localDirection);
618 if (element.invertDirection) worldDirection = -worldDirection;
619 output.value = {std::clamp(worldDirection.x * .5f + .5f, 0.f, 1.f),
620 std::clamp(worldDirection.z * .5f + .5f, 0.f, 1.f), element.motionPower,
621 element.intensity * remappedAlpha * input.elementParams.a * input.vertexColor.a * volume};
622 output.colorMask = std::uint8_t(element.motionMode);
623 output.alphaOperation = VegetationScenePixelBlend::Add;
624 } else if (element.kind == "vertex-conform-model") {
625 output.value = {0.f, 0.f, input.worldPosition.y + propertyScalar(element, "_HeightOffsetValue", 0.f),
626 element.intensity * volume};
627 output.colorMask = 2;
628 } else if (element.kind == "vertex-conform-simple") {
629 output.value = {0.f, 0.f, input.mainSample.r * propertyScalar(element, "_HeightValue", 1.f) +
630 propertyScalar(element, "_HeightOffsetValue", 0.f),
631 element.intensity * volume};
632 output.colorMask = 2;
633 } else if (element.kind == "vertex-conform-terrain") {
634 output.value = {0.f, 0.f, input.terrainHeight + propertyScalar(element, "_HeightOffsetValue", 0.f),
635 element.intensity * volume};
636 output.colorMask = 2;
637 } else if (element.kind == "vertex-height-offset") {
638 output.value = {0.f, 0.f, propertyScalar(element, "_HeightOffsetValue", 0.f) * remappedAlpha *
639 element.intensity * volume,
640 0.f};
641 output.colorMask = 2;
643 } else if (element.kind == "vertex-height") {
644 output.value = {0.f, 0.f, scalar * propertyScalar(element, "_HeightValue", 1.f) +
645 propertyScalar(element, "_HeightOffsetValue", 0.f),
646 alpha};
647 output.colorMask = 2;
648 } else if (element.kind == "vertex-orientation-model") {
649 const glm::vec3 normal = glm::length(input.worldNormal) > 0.f ? glm::normalize(input.worldNormal)
650 : glm::vec3(0.f, 1.f, 0.f);
651 output.value = {normal.x * .5f + .5f, normal.z * .5f + .5f, 0.f, element.intensity * volume};
652 output.colorMask = 12;
653 } else if (element.kind == "vertex-orientation-terrain") {
654 output.value = {input.mainSample.r, input.mainSample.b, 0.f, element.intensity * volume};
655 output.colorMask = 12;
656 } else {
658 Diagnostic::error(DiagnosticCode::Unsupported, "TVE Element pixel shader is not implemented", element.kind,
659 {}, "asset.graphics.vegetation-scene"));
660 }
661 if (!finite(output.value))
663 Diagnostic::error(DiagnosticCode::InvalidArgument, "vegetation element pixel evaluation overflowed", {}, {},
664 "asset.graphics.vegetation-scene"));
666}
667
668glm::vec4 composeVegetationSceneElementPixel(glm::vec4 destination,
669 const VegetationSceneElementPixel& source) noexcept {
670 auto apply = [&](float dst, float src, VegetationScenePixelBlend operation) {
671 switch (operation) {
672 case VegetationScenePixelBlend::Alpha: return src * source.value.a + dst * (1.f - source.value.a);
673 case VegetationScenePixelBlend::Multiply: return src * dst;
674 case VegetationScenePixelBlend::Add: return src + dst;
675 case VegetationScenePixelBlend::Replace: return src;
676 }
677 return dst;
678 };
679 glm::vec4 result = destination;
680 if (source.colorMask & 8) result.r = apply(destination.r, source.value.r, source.rgbOperation);
681 if (source.colorMask & 4) result.g = apply(destination.g, source.value.g, source.rgbOperation);
682 if (source.colorMask & 2) result.b = apply(destination.b, source.value.b, source.rgbOperation);
683 if (source.colorMask & 1) result.a = apply(destination.a, source.value.a, source.alphaOperation);
684 return result;
685}
686
688 const LoadedVegetationScene& scene, const std::map<std::string, graphics::VegetationMask>& masks,
689 graphics::VegetationChannel channel, const graphics::VegetationChannelAtlas& base, std::uint8_t layer,
690 std::span<const glm::vec3> worldNormals, std::span<const float> terrainHeights,
691 std::span<const glm::vec4> noiseSamples) {
692 const std::uint64_t pixelCount64 = std::uint64_t(base.width) * base.height;
693 if (base.width == 0 || base.height == 0 || base.width > 2048 || base.height > 2048 ||
694 pixelCount64 > 4 * 1024 * 1024 || !std::isfinite(base.center.x) || !std::isfinite(base.center.y) ||
695 !std::isfinite(base.center.z) || !std::isfinite(base.extent.x) || !std::isfinite(base.extent.y) ||
696 !std::isfinite(base.extent.z) || base.extent.x <= 0.f || base.extent.y <= 0.f || base.extent.z <= 0.f ||
697 layer > 8)
699 Diagnostic::error(DiagnosticCode::InvalidArgument, "vegetation scene channel geometry or layer is invalid",
700 {}, {}, "asset.graphics.vegetation-scene"));
701 const std::size_t pixelCount = std::size_t(pixelCount64);
702 if (base.pixels.size() != pixelCount ||
703 (!worldNormals.empty() && worldNormals.size() != pixelCount) ||
704 (!terrainHeights.empty() && terrainHeights.size() != pixelCount) ||
705 (!noiseSamples.empty() && noiseSamples.size() != pixelCount))
707 Diagnostic::error(DiagnosticCode::InvalidArgument, "vegetation scene channel input sizes are inconsistent",
708 {}, {}, "asset.graphics.vegetation-scene"));
709 try {
711 const std::size_t channelIndex = static_cast<std::size_t>(channel);
712 const double stepX = 2.0 * base.extent.x / base.width;
713 const double stepZ = 2.0 * base.extent.z / base.height;
714 for (const auto& element : scene.elements) {
715 if (!element.enabled || element.channel != channelIndex || !(element.layers & (1u << layer))) continue;
716 const graphics::VegetationMask* mask = nullptr;
717 if (!element.textureGuid.empty()) {
718 const auto found = masks.find(element.textureGuid);
719 if (found == masks.end())
721 Diagnostic::error(DiagnosticCode::NotFound, "vegetation scene element mask is missing",
722 element.textureGuid, {}, "asset.graphics.vegetation-scene"));
723 mask = &found->second;
724 }
725 for (std::uint32_t y = 0; y < base.height; ++y) {
726 for (std::uint32_t x = 0; x < base.width; ++x) {
727 const std::size_t index = std::size_t(y) * base.width + x;
728 const glm::vec3 world(float(double(base.center.x) - base.extent.x + (x + .5) * stepX),
729 base.center.y,
730 float(double(base.center.z) - base.extent.z + (y + .5) * stepZ));
731 const glm::vec3 delta = world - glm::vec3(element.position[0], element.position[1], element.position[2]);
732 const glm::vec3 local = inverseRotate(element.rotation, delta) /
733 glm::vec3(element.scale[0], element.scale[1], element.scale[2]);
734 const glm::vec2 uv(local.x + .5f, local.z + .5f);
735 if (uv.x < 0.f || uv.x > 1.f || uv.y < 0.f || uv.y > 1.f) continue;
737 input.localUv = uv;
738 const bool flipped = element.kind == "motion-interaction" || element.kind == "motion-advanced" ||
739 element.kind == "vertex-conform-simple" ||
740 element.kind == "vertex-orientation-terrain" ||
741 element.kind == "vertex-height-offset";
742 input.mainSample = mask ? sampleMask(*mask, flipped ? glm::vec2(1.f) - uv : uv) : glm::vec4(1.f);
743 input.worldPosition = world;
744 if (!worldNormals.empty()) input.worldNormal = worldNormals[index];
745 if (!terrainHeights.empty()) input.terrainHeight = terrainHeights[index];
746 if (!noiseSamples.empty()) input.noiseSample = noiseSamples[index];
747 input.season = scene.control.season;
748 if (element.volumeFade) {
749 const glm::vec2 atlasUv((world.x - base.center.x) / (2.f * base.extent.x) + .5f,
750 (world.z - base.center.z) / (2.f * base.extent.z) + .5f);
751 const glm::vec2 edge = glm::abs(atlasUv * 2.002f - 1.001f);
752 if (scene.volume.edgeFade >= 1.f)
753 input.volumeFade = edge.x <= 1.f && edge.y <= 1.f ? 1.f : 0.f;
754 else {
755 const glm::vec2 faded = glm::clamp((edge - scene.volume.edgeFade) /
756 (1.f - scene.volume.edgeFade),
757 glm::vec2(0.f), glm::vec2(1.f));
758 input.volumeFade = 1.f - std::clamp(glm::dot(faded, faded), 0.f, 1.f);
759 }
760 }
764 }
765 }
766 }
768 } catch (const std::bad_alloc&) {
770 Diagnostic::error(DiagnosticCode::Failed, "vegetation scene channel allocation failed", {}, {},
771 "asset.graphics.vegetation-scene"));
772 }
773}
774
776 const LoadedVegetationScene& scene, const std::map<std::string, graphics::VegetationMask>& masks,
777 const graphics::VegetationAtlas& base, std::array<std::uint8_t, 4> layers,
778 std::span<const glm::vec3> worldNormals, std::span<const float> terrainHeights,
779 std::span<const glm::vec4> noiseSamples) {
781 output.width = base.width;
782 output.height = base.height;
783 output.center = base.center;
784 output.extent = base.extent;
785 for (std::size_t index = 0; index < 4; ++index) {
786 graphics::VegetationChannelAtlas channelBase{base.width, base.height, base.center, base.extent,
787 base.channels[index]};
788 auto baked = bakeVegetationSceneChannel(scene, masks, static_cast<graphics::VegetationChannel>(index),
789 channelBase, layers[index], worldNormals, terrainHeights, noiseSamples);
790 if (!baked) return Result<graphics::VegetationAtlas>::failure(baked.status());
791 output.channels[index] = std::move(baked).takeValue().pixels;
792 }
794}
795
798 const std::uint64_t pixelCount64 = std::uint64_t(tveAtlas.width) * tveAtlas.height;
799 if (tveAtlas.width == 0 || tveAtlas.height == 0 || tveAtlas.width > 2048 || tveAtlas.height > 2048 ||
800 pixelCount64 > 4 * 1024 * 1024 || !std::isfinite(tveAtlas.center.x) || !std::isfinite(tveAtlas.center.y) ||
801 !std::isfinite(tveAtlas.center.z) || !std::isfinite(tveAtlas.extent.x) ||
802 !std::isfinite(tveAtlas.extent.y) || !std::isfinite(tveAtlas.extent.z) || tveAtlas.extent.x <= 0.f ||
803 tveAtlas.extent.y <= 0.f || tveAtlas.extent.z <= 0.f)
805 Diagnostic::error(DiagnosticCode::InvalidArgument, "TVE vegetation channel geometry is invalid", {}, {},
806 "asset.graphics.vegetation-scene"));
807 const std::size_t pixelCount = std::size_t(pixelCount64);
808 if (tveAtlas.pixels.size() != pixelCount ||
809 std::any_of(tveAtlas.pixels.begin(), tveAtlas.pixels.end(), [](glm::vec4 value) { return !finite(value); }))
811 Diagnostic::error(DiagnosticCode::InvalidArgument, "TVE vegetation channel pixels are invalid", {}, {},
812 "asset.graphics.vegetation-scene"));
813 try {
814 graphics::VegetationChannelAtlas native = tveAtlas;
816 for (auto& motion : native.pixels) {
817 motion.x = motion.x * 2.f - 1.f;
818 motion.y = motion.y * 2.f - 1.f;
819 }
820 }
822 } catch (const std::bad_alloc&) {
824 Diagnostic::error(DiagnosticCode::Failed, "native vegetation channel allocation failed", {}, {},
825 "asset.graphics.vegetation-scene"));
826 }
827}
828
830 const graphics::VegetationAtlas& tveAtlas) {
832 native.width = tveAtlas.width;
833 native.height = tveAtlas.height;
834 native.center = tveAtlas.center;
835 native.extent = tveAtlas.extent;
836 for (std::size_t index = 0; index < 4; ++index) {
837 graphics::VegetationChannelAtlas source{tveAtlas.width, tveAtlas.height, tveAtlas.center, tveAtlas.extent,
838 tveAtlas.channels[index]};
840 if (!converted) return Result<graphics::VegetationAtlas>::failure(converted.status());
841 native.channels[index] = std::move(converted).takeValue().pixels;
842 }
843 return Result<graphics::VegetationAtlas>::success(std::move(native));
844}
845
847 graphics::IResourceFactory& factory, const graphics::PbrSurface& baseSurface,
848 const graphics::VegetationMotion& baseMotion, const LoadedVegetationScene& scene,
849 const std::map<std::string, graphics::VegetationMask>& masks, const VegetationSceneGpuBuild& build) {
850 if (build.sourceRevision == 0)
852 Diagnostic::error(DiagnosticCode::InvalidArgument, "vegetation scene GPU revision must be nonzero", {}, {},
853 "asset.graphics.vegetation-scene"));
854 auto projected = projectVegetationScene(baseSurface, baseMotion, scene);
856 try {
857 std::array<std::array<graphics::VegetationChannelAtlas, 9>, 4> nativeChannels;
858 for (std::size_t channel = 0; channel < nativeChannels.size(); ++channel) {
859 const auto& input = build.channels[channel];
860 for (std::uint8_t layer = 0; layer < nativeChannels[channel].size(); ++layer) {
861 auto tve = bakeVegetationSceneChannel(scene, masks,
862 static_cast<graphics::VegetationChannel>(channel),
863 input.baseLayers[layer], layer, input.worldNormals,
864 input.terrainHeights, input.noiseSamples);
865 if (!tve) return Result<std::unique_ptr<VegetationSceneGpuRuntime>>::failure(tve.status());
867 static_cast<graphics::VegetationChannel>(channel), tve.value());
868 if (!native) return Result<std::unique_ptr<VegetationSceneGpuRuntime>>::failure(native.status());
869 nativeChannels[channel][layer] = std::move(native).takeValue();
870 }
871 }
872
873 auto runtime = std::unique_ptr<VegetationSceneGpuRuntime>(new VegetationSceneGpuRuntime(factory));
874 runtime->projection_ = std::move(projected).takeValue();
875 runtime->sourceRevision_ = build.sourceRevision;
876 auto uploaded = graphics::uploadVegetationGpuFieldSet(factory, nativeChannels[0], nativeChannels[1],
877 nativeChannels[2], nativeChannels[3],
878 build.sourceRevision);
879 if (!uploaded) return Result<std::unique_ptr<VegetationSceneGpuRuntime>>::failure(uploaded.status());
880 runtime->fields_ = std::move(uploaded).takeValue();
881
883 const auto& motion = runtime->projection_.surface.vegetationMotion;
884 gpu.motionDirection = motion.globalDirection;
886 gpu.time = runtime->projection_.motion.time;
892 gpu.cameraFadeMin = runtime->projection_.surface.vegetationAlpha.cameraFadeMin;
893 gpu.cameraFadeMax = runtime->projection_.surface.vegetationAlpha.cameraFadeMax;
894 gpu.fadeNoiseTiling = runtime->projection_.surface.vegetationAlpha.noiseTiling;
895 auto bound = graphics::bindVegetationGpuFields(runtime->projection_.surface, runtime->fields_,
896 build.sourceRevision, gpu);
897 if (!bound) {
898 auto released = runtime->release();
899 if (!released)
901 Diagnostic::error(DiagnosticCode::Failed, "vegetation scene GPU binding rollback failed", {}, {},
902 "asset.graphics.vegetation-scene"));
903 return Result<std::unique_ptr<VegetationSceneGpuRuntime>>::failure(bound.status());
904 }
905 return Result<std::unique_ptr<VegetationSceneGpuRuntime>>::success(std::move(runtime));
906 } catch (const std::bad_alloc&) {
908 Diagnostic::error(DiagnosticCode::Failed, "vegetation scene GPU runtime allocation failed", {}, {},
909 "asset.graphics.vegetation-scene"));
910 }
911}
912
914 std::uint64_t expectedRevision, const graphics::PbrSurface& baseSurface,
915 const graphics::VegetationMotion& baseMotion, const LoadedVegetationScene& scene,
916 const std::map<std::string, graphics::VegetationMask>& masks, const VegetationSceneGpuBuild& build) {
917 if (!factory_)
919 Diagnostic::error(DiagnosticCode::Conflict, "released vegetation scene runtime cannot be replaced", {}, {},
920 "asset.graphics.vegetation-scene"));
921 if (expectedRevision != sourceRevision_)
923 Diagnostic::error(DiagnosticCode::Conflict, "vegetation scene GPU revision changed before replacement", {},
924 {}, "asset.graphics.vegetation-scene"));
925 if (build.sourceRevision <= sourceRevision_)
927 Diagnostic::error(DiagnosticCode::InvalidArgument, "vegetation scene replacement revision must increase",
928 {}, {}, "asset.graphics.vegetation-scene"));
929
930 auto candidate = create(*factory_, baseSurface, baseMotion, scene, masks, build);
931 if (!candidate) return Result<VegetationSceneGpuPublication>::failure(candidate.status());
932 try {
933 retiredFields_.reserve(retiredFields_.size() + 1);
934 retiredFields_.push_back(std::move(fields_));
935 } catch (const std::bad_alloc&) {
937 Diagnostic::error(DiagnosticCode::Failed, "vegetation scene replacement retirement allocation failed", {},
938 {}, "asset.graphics.vegetation-scene"));
939 }
940
941 fields_ = std::move(candidate.value()->fields_);
942 std::swap(projection_, candidate.value()->projection_);
943 sourceRevision_ = build.sourceRevision;
944 candidate.value()->factory_ = nullptr;
945
946 for (auto current = retiredFields_.begin(); current != retiredFields_.end();) {
947 auto released = graphics::releaseVegetationGpuFieldSet(*factory_, *current);
948 if (released)
949 current = retiredFields_.erase(current);
950 else
951 ++current;
952 }
953 return Result<VegetationSceneGpuPublication>::success({sourceRevision_, retiredFields_.size()});
954}
955
957 auto released = release();
958 if (!released)
959 std::fprintf(stderr, "vegetation scene GPU release failed: %s\n", released.error()->message().c_str());
960}
961
963 if (!factory_) return Result<void>::success();
964 projection_.surface.vegetationExtras.texture = nullptr;
965 projection_.surface.vegetationColors.texture = nullptr;
966 projection_.surface.vegetationMotion.texture = nullptr;
967 projection_.surface.vegetationVertex.texture = nullptr;
968 auto released = graphics::releaseVegetationGpuFieldSet(*factory_, fields_);
969 Status firstFailure;
970 if (!released) firstFailure = released.status();
971 for (auto current = retiredFields_.begin(); current != retiredFields_.end();) {
973 if (retired)
974 current = retiredFields_.erase(current);
975 else {
976 if (firstFailure.isSuccess()) firstFailure = retired.status();
977 ++current;
978 }
979 }
980 if (firstFailure.isFailure()) return Result<void>::failure(firstFailure);
981 factory_ = nullptr;
982 return Result<void>::success();
983}
984} // namespace eve::asset_graphics
ActionParameterOperation operation
double value
Value::Object payload
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
std::string output
double volume
const std::string & s
int mask
float uv
Vec3 projected
Definition CaveMesh.cpp:122
std::map< std::string, Var > values
Backend-neutral canonical EVIMG decoding.
Runtime decoding and projection of TVE scene-manager state.
EvpackChunkInput input
Definition Evpack.cpp:170
float maximum[3]
float minimum[3]
bool retired
vk::UniqueImage image
float u
Definition Grass.cpp:233
std::int32_t c
TokenKind kind
std::string local
size_t offset
std::array< float, 4 > rotation
std::array< float, 3 > position
bool required
std::string name
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
Texture * normal
bool finite
TileLayer * layer
World3D * world
std::vector< std::string > fields
Definition PlayHost.cpp:111
std::weak_ptr< PrimitiveScene > scene
float t
std::unordered_map< std::uint32_t, std::uint32_t > remap
RoadLaneDirection direction
const RoadEdge * edge
bool found
JSON-free versioned runtime metadata codec.
double current
std::string element
TacticalUnit * unit
float size
Definition TreeMesh.cpp:156
float(ui::Theme::* member)[4]
uint32_t index
const UnitySourceAsset & source
const AssetImportLimits & limits
float m[16]
Stable asset identity backed by PersistentId.
Definition ResourceRef.h:53
static Result< AssetRef > parse(std::string_view text)
Parse an asset://<canonical UUID> reference.
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.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
bool isSuccess() const noexcept
Whether the operation completed without a failure outcome.
Definition Status.h:101
bool isFailure() const noexcept
Whether the operation has a failure outcome.
Definition Status.h:118
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
static Value object(Object value)
Compatibility factory for an object value.
Definition Value.h:114
Immutable reader retaining an admitted pack by shared ownership.
Result< RuntimeAssetPayload > read(const AssetRef &asset, std::string_view expectedType, const EvpackCapabilities &capabilities, std::uint64_t maximumDecodedBytes) const
Resolve, type-check, variant-select and decode a canonical runtime asset.
Result< LoadedVegetationScene > load(const AssetRef &asset, const asset::EvpackCapabilities &capabilities, std::uint64_t maximumDecodedBytes=1024 *1024) const
Decode and validate one complete scene state without mutating runtime objects.
Owning GPU publication of one immutable TVE manager scene. The resource factory, mask map and scene a...
const graphics::VegetationMotion & motion() const noexcept
Borrow the projected CPU motion snapshot; valid for this runtime's lifetime.
static Result< std::unique_ptr< VegetationSceneGpuRuntime > > create(graphics::IResourceFactory &factory, const graphics::PbrSurface &baseSurface, const graphics::VegetationMotion &baseMotion, const LoadedVegetationScene &scene, const std::map< std::string, graphics::VegetationMask > &masks, const VegetationSceneGpuBuild &build)
Bake, convert, upload and bind all field layers as one atomic candidate.
Result< VegetationSceneGpuPublication > replace(std::uint64_t expectedRevision, const graphics::PbrSurface &baseSurface, const graphics::VegetationMotion &baseMotion, const LoadedVegetationScene &scene, const std::map< std::string, graphics::VegetationMask > &masks, const VegetationSceneGpuBuild &build)
Atomically replace this runtime after validating its observed revision. A complete candidate is built...
Result< void > release()
Invalidate material borrows and release all fields; failed entries remain owned for retry.
Texture / mesh / shader / canvas creation and release.
Result< graphics::VegetationChannelAtlas > convertVegetationSceneChannelToNative(graphics::VegetationChannel channel, const graphics::VegetationChannelAtlas &tveAtlas)
Convert one fully composed TVE channel atlas to EVEngine conventions.
Result< graphics::VegetationChannelAtlas > bakeVegetationSceneChannel(const LoadedVegetationScene &scene, const std::map< std::string, graphics::VegetationMask > &masks, graphics::VegetationChannel channel, const graphics::VegetationChannelAtlas &base, std::uint8_t layer, std::span< const glm::vec3 > worldNormals, std::span< const float > terrainHeights, std::span< const glm::vec4 > noiseSamples)
Bake one TVE manager channel at its own resolution and world mapping.
Result< VegetationSceneElementPixel > evaluateVegetationSceneElementPixel(const VegetationSceneElement &element, const VegetationSceneElementPixelInput &input)
Evaluate one TVE Element fragment using caller-supplied texture, instance and particle inputs.
Result< VegetationSceneProjection > projectVegetationScene(const graphics::PbrSurface &baseSurface, const graphics::VegetationMotion &baseMotion, const LoadedVegetationScene &scene)
Project validated TVE manager controls onto one material/motion snapshot.
Result< graphics::VegetationAtlas > convertVegetationSceneAtlasToNative(const graphics::VegetationAtlas &tveAtlas)
Convert a fully composed TVE render-target atlas to EVEngine field channel conventions.
VegetationScenePixelBlend
Detached TVE render-target source value and independent hardware blend policy.
Result< graphics::VegetationAtlas > bakeVegetationSceneElements(const LoadedVegetationScene &scene, const std::map< std::string, graphics::VegetationMask > &masks, const graphics::VegetationAtlas &base, std::array< std::uint8_t, 4 > layers, std::span< const glm::vec3 > worldNormals, std::span< const float > terrainHeights, std::span< const glm::vec4 > noiseSamples)
Rasterize the ordered TVE Element list over an owning four-channel atlas candidate.
Result< std::map< std::string, graphics::VegetationMask > > loadVegetationSceneElementMasks(const asset::EvpackResourceReader &reader, const LoadedVegetationScene &scene, const asset::EvpackCapabilities &capabilities, std::uint64_t maximumDecodedBytes)
Decode every runtime texture used by admitted TVE Element shaders into owning linear masks.
glm::vec4 composeVegetationSceneElementPixel(glm::vec4 destination, const VegetationSceneElementPixel &source) noexcept
Apply one evaluated source pixel to a destination with TVE's separate blend factors and ColorMask.
bool validGuid(std::string_view value)
Valid guid.
Result< Value > decodeRuntimeDefinition(std::span< const std::uint8_t > bytes, const RuntimeDefinitionLimits &limits)
Decode a bounded EVDEF\0\1 metadata tree without a JSON parser.
Result< DecodedEvpackImage > decodeEvpackImage(const EvpackResourceReader &reader, const AssetRef &image, const EvpackCapabilities &capabilities, const EvpackImageDecodeLimits &limits)
Decode and validate canonical eve.image/3 data with eve.image/2 compatibility.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
Result< VegetationDetailSettings > restoreVegetationDetails(const Value &document)
Decode one exact Global Details document transactionally.
Result< void > releaseVegetationGpuFieldSet(IResourceFactory &factory, VegetationGpuFieldSet &set)
Release every texture still present in a detached field set. Successful entries are nulled....
VegetationChannel
Independent vegetation field channels; values use linear color and world units.
Result< VegetationGpuFieldSet > uploadVegetationGpuFieldSet(IResourceFactory &factory, const VegetationField &field, glm::vec3 center, glm::vec3 extent, uint32_t width, uint32_t height, bool snapToTexel)
Bake and upload all four nine-layer projections as one publication. Earlier uploads are released if a...
Result< void > validatePbrSurface(const PbrSurface &s)
Validate finite material factors and sampler enums without changing the input.
Definition PbrSurface.cpp:4
Result< void > bindVegetationGpuFields(PbrSurface &surface, const VegetationField &field, const VegetationExtrasGpuAtlas &extras, const VegetationColorsGpuAtlas &colors, const VegetationMotionGpuAtlas &motion, const VegetationVertexGpuAtlas &vertex, const VegetationGpuRuntime &runtime)
Atomically bind four coherent GPU projections to an owning PBR parameter snapshot.
Result< VegetationDetailRuntime > configureVegetationDetails(const PbrSurface &baseSurface, const VegetationMotion &baseMotion, const VegetationDetailSettings &s)
Validate and project TVE Global Details into native PBR and motion snapshots.
bool enabled
Actual runtime device capabilities used for variant selection.
Definition Evpack.h:92
Bounds applied before allocating decoded image bytes.
One decoded runtime chunk with its admitted semantic metadata.
Structural and allocation limits for untrusted EVDEF metadata.
Fully validated, detached scene-manager candidate with no archive or GPU borrows.
Explicit per-draw inputs consumed by one TVE Element fragment evaluation.
Detached TVE render-target source value and independent hardware blend policy.
One normalized Unity TVEElement definition awaiting texture realization.
One normalized Unity TVEElement definition awaiting texture realization.
std::vector< VegetationSceneElementProperty > properties
std::array< std::array< float, 4 >, 4 > seasons
Explicit detached inputs for publishing all nine TVE scene-manager field layers.
std::array< VegetationSceneChannelGpuBuild, 4 > channels
Detached PBR and CPU-motion snapshot ready for atomic material publication.
Owning material parameter snapshot; texture pointers remain borrowed. Base color/metallic/roughness c...
Definition PbrSurface.h:218
PbrVegetationMotion vegetationMotion
Definition PbrSurface.h:259
PbrVegetationExtras vegetationExtras
Definition PbrSurface.h:256
PbrVegetationVertex vegetationVertex
Definition PbrSurface.h:258
PbrVegetationColors vegetationColors
Definition PbrSurface.h:257
Four owning arrays in row-major X/Z order at texel centers.
std::array< std::vector< glm::vec4 >, 4 > channels
One owning vegetation channel in row-major X/Z order at texel centers. Channel atlases may use indepe...
Runtime-owned global inputs shared by one coherent vegetation field binding. Time is explicit for det...
Owning linear RGBA mask, sampled bilinearly with clamped addressing.
Source-equivalent plant motion parameters with explicit time and texture inputs. All data is owned....
glm::vec4 color