载入中...
搜索中...
未找到
TerrainTreePlacement.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
6#include <type_traits>
7#include <unordered_set>
8
9#include "procgen/PointSet.h"
13
14namespace eve::procgen {
15namespace {
16class PcgXorshiftPlus {
17public:
18 explicit PcgXorshiftPlus(std::int32_t seed) {
19 const std::uint32_t value = seed == 0 ? 1U : static_cast<std::uint32_t>(seed);
20 stateA_ = UINT64_C(181353) * value;
21 stateB_ = UINT64_C(7) * value;
22 }
23 float next() {
24 std::uint64_t x = stateA_, y = stateB_;
25 stateA_ = y;
26 x ^= x << 23;
27 x ^= x >> 17;
28 x ^= y ^ (y >> 26);
29 stateB_ = x;
30 return static_cast<float>(x + y) / static_cast<float>(std::numeric_limits<std::uint64_t>::max());
31 }
32 float next(float minimum, float maximum) { return minimum + next() * (maximum - minimum); }
33
34private:
35 std::uint64_t stateA_ = 0, stateB_ = 0;
36};
37
38std::uint64_t mix(std::uint64_t value) {
39 value = (value ^ (value >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
40 value = (value ^ (value >> 27)) * UINT64_C(0x94d049bb133111eb);
41 return value ^ (value >> 31);
42}
43bool normalized(float value) { return std::isfinite(value) && value >= 0 && value <= 1; }
44bool validScaleMode(TerrainTreeScaleMode value) {
46}
47bool validYOffsetMode(TerrainTreeYOffsetMode value) {
49}
50int nearestEven(double value) {
51 const double lower = std::floor(value), fraction = value - lower;
52 return int(lower + (fraction > 0.5 || (fraction == 0.5 && std::fmod(lower, 2.0) != 0)));
53}
54std::uint64_t stableNameHash(const std::string& name) {
55 std::uint64_t value = UINT64_C(1469598103934665603);
56 for (unsigned char byte : name) value = (value ^ byte) * UINT64_C(1099511628211);
57 return value;
58}
59} // namespace
60
63 using namespace raster_detail;
64 if (!validRaster(fitness) || !validRaster(heights) || !std::isfinite(s.originX) ||
65 !std::isfinite(s.originZ) || !std::isfinite(s.width) || s.width <= 0 || !std::isfinite(s.depth) ||
66 s.depth <= 0 || !std::isfinite(s.heightScale) || !std::isfinite(s.spacing) || s.spacing <= 0 ||
67 !std::isfinite(s.spawnDensity) || s.spawnDensity <= 0 || !normalized(s.jitterPercent) ||
68 !normalized(s.failureRate) || !normalized(s.minimumFitness) || !std::isfinite(s.seaLevel) ||
69 !std::isfinite(s.customOffset) || !std::isfinite(s.minimumYOffset) || !std::isfinite(s.maximumYOffset) ||
70 s.maximumYOffset < s.minimumYOffset || !validScaleMode(s.scaleMode) || !validYOffsetMode(s.yOffsetMode) ||
71 !std::isfinite(s.minimumWidth) || s.minimumWidth <= 0 || !std::isfinite(s.maximumWidth) ||
72 s.maximumWidth < s.minimumWidth || !std::isfinite(s.minimumHeight) || s.minimumHeight <= 0 ||
73 !std::isfinite(s.maximumHeight) || s.maximumHeight < s.minimumHeight ||
74 !normalized(s.widthRandomPercentage) || !normalized(s.heightRandomPercentage) || !normalized(s.healthyR) ||
75 !normalized(s.healthyG) || !normalized(s.healthyB) || !normalized(s.healthyA) || !normalized(s.dryR) ||
76 !normalized(s.dryG) || !normalized(s.dryB) || !normalized(s.dryA) || !std::isfinite(s.bendFactor) ||
77 s.bendFactor < 0 || !std::isfinite(s.boundsRadius) || s.boundsRadius <= 0 || s.namespaceId == 0 ||
78 s.asset.empty() || s.maxPoints < 0)
79 return invalid("terrain.treePoints: valid rasters, domain, prototype, probabilities and budget required");
80 if (!std::all_of(fitness.data().begin(), fitness.data().end(), [](float value) {
81 return value >= 0 && value <= 1;
82 }))
83 return invalid("terrain.treePoints: fitness raster must be normalized");
84
85 const double increment = double(s.spacing) / double(s.spawnDensity);
86 if (!isRepresentable(increment) || increment <= 0)
87 return invalid("terrain.treePoints: effective spacing is not representable");
88 const double xSteps = std::floor(double(s.width) / increment) + 1;
89 const double zSteps = std::floor(double(s.depth) / increment) + 1;
90 if (!std::isfinite(xSteps) || !std::isfinite(zSteps) || xSteps > std::numeric_limits<int>::max() ||
91 zSteps > std::numeric_limits<int>::max() || xSteps * zSteps > std::numeric_limits<int>::max())
92 return invalid("terrain.treePoints: candidate grid exceeds supported size");
93
94 PcgXorshiftPlus random(s.seed);
95 PointSet next;
96 next.reserve(std::min<std::size_t>(static_cast<std::size_t>(xSteps * zSteps), static_cast<std::size_t>(s.maxPoints)));
97 const double jitter = increment * s.jitterPercent;
98 int emitted = 0;
99 for (int xi = 0; xi < static_cast<int>(xSteps); ++xi) {
100 for (int zi = 0; zi < static_cast<int>(zSteps); ++zi) {
101 if (random.next() < s.failureRate) continue;
102 const double localX = xi * increment + random.next(float(-jitter), float(jitter));
103 const double localZ = zi * increment + random.next(float(-jitter), float(jitter));
104 if (localX < 0 || localZ < 0 || localX > s.width || localZ > s.depth) continue;
105 const double u = localX / s.width, v = localZ / s.depth;
106 const double strength = textureSample(fitness, u, v);
107 if (random.next(s.minimumFitness, 1) > strength) continue;
108 if (emitted >= s.maxPoints) return invalid("terrain.treePoints: point budget exceeded");
109
110 const std::uint64_t cell = std::uint64_t(xi) * std::uint64_t(static_cast<int>(zSteps)) + std::uint64_t(zi);
112 point.id = mix(mix(s.namespaceId + UINT64_C(0x9e3779b97f4a7c15)) ^ (cell + 1));
113 if (point.id == 0) return invalid("terrain.treePoints: reserved zero identity; choose another namespace");
114 const double terrainY = sample(heights, u, v) * double(s.heightScale);
115 double worldY = terrainY;
116 if (!s.snapToTerrain) {
117 if (s.yOffsetMode == TerrainTreeYOffsetMode::SeaLevel) worldY = s.seaLevel;
118 if (s.yOffsetMode == TerrainTreeYOffsetMode::Custom) worldY = s.customOffset;
119 worldY += random.next(s.minimumYOffset, s.maximumYOffset);
120 }
121 double width = s.minimumWidth, height = s.minimumHeight;
122 if (s.scaleMode == TerrainTreeScaleMode::Fitness ||
124 width = std::lerp(double(s.minimumWidth), double(s.maximumWidth), strength);
125 height = std::lerp(double(s.minimumHeight), double(s.maximumHeight), strength);
126 } else if (s.scaleMode == TerrainTreeScaleMode::Random) {
127 const double value = random.next();
128 width = std::lerp(double(s.minimumWidth), double(s.maximumWidth), value);
129 height = std::lerp(double(s.minimumHeight), double(s.maximumHeight), value);
130 }
132 const double value = random.next();
133 width *= std::lerp(1.0 - s.widthRandomPercentage, 1.0 + s.widthRandomPercentage, value);
134 height *= std::lerp(1.0 - s.heightRandomPercentage, 1.0 + s.heightRandomPercentage, value);
135 }
136 const double worldX = double(s.originX) + localX, worldZ = double(s.originZ) + localZ;
137 if (!isRepresentable(worldX) || !isRepresentable(worldY) || !isRepresentable(worldZ) ||
139 return invalid("terrain.treePoints: generated transform is not representable");
140 point.x = float(worldX);
141 point.y = float(worldY);
142 point.z = float(worldZ);
143 point.yaw = random.next(0, 360);
144 point.scaleX = point.scaleZ = float(width);
145 point.scaleY = float(height);
146 point.density = float(strength);
147 point.colorR = float(std::lerp(double(s.dryR), double(s.healthyR), strength));
148 point.colorG = float(std::lerp(double(s.dryG), double(s.healthyG), strength));
149 point.colorB = float(std::lerp(double(s.dryB), double(s.healthyB), strength));
150 point.colorA = float(std::lerp(double(s.dryA), double(s.healthyA), strength));
151 point.seed = static_cast<std::uint32_t>(mix(point.id ^ static_cast<std::uint32_t>(s.seed)));
152 const float radius = float(double(s.boundsRadius) * width);
153 if (!std::isfinite(radius)) return invalid("terrain.treePoints: generated bounds are not representable");
154 point.boundsMinX = point.boundsMinZ = -radius;
155 point.boundsMaxX = point.boundsMaxZ = radius;
156 point.boundsMaxY = float(height);
157 const int row = next.appendPoint(point);
158 auto asset = next.trySetStringAttribute(row, "asset", s.asset);
159 if (!asset.ok()) return Result<int>::failure(asset.status());
160 auto source = next.trySetStringAttribute(row, "spawnNamespace", std::to_string(s.namespaceId));
161 if (!source.ok()) return Result<int>::failure(source.status());
162 auto candidate = next.trySetIntAttribute(row, "treeCandidate", static_cast<std::int64_t>(cell));
163 if (!candidate.ok()) return Result<int>::failure(candidate.status());
164 auto bend = next.trySetFloatAttribute(row, "bendFactor", s.bendFactor);
165 if (!bend.ok()) return Result<int>::failure(bend.status());
166 ++emitted;
167 }
168 }
169 static_assert(std::is_nothrow_move_assignable_v<PointSet>);
170 output = std::move(next);
171 return Result<int>::success(emitted);
172}
173
176 using namespace raster_detail;
177 if (!validRaster(fitness) || !std::all_of(fitness.data().begin(), fitness.data().end(), [](float value) {
178 return value >= 0 && value <= 1;
179 }) ||
180 !std::isfinite(s.originX) || !std::isfinite(s.originZ) || !std::isfinite(s.width) || s.width <= 0 ||
181 !std::isfinite(s.depth) || s.depth <= 0 || !normalized(s.minimumFitness) || s.asset.empty())
182 return invalid("terrain.removeTreePoints: valid fitness, domain, threshold and asset required");
183 PointSet next;
184 next.reserve(static_cast<std::size_t>(input.getCount()));
185 int removed = 0;
186 for (int i = 0; i < input.getCount(); ++i) {
187 const auto& point = input.points()[static_cast<std::size_t>(i)];
188 bool erase = false;
189 if (input.getStringAttribute(i, "asset", "") == s.asset) {
190 const double u = (double(point.x) - s.originX) / s.width;
191 const double v = (double(point.z) - s.originZ) / s.depth;
192 if (u >= 0 && u <= 1 && v >= 0 && v <= 1) erase = textureSample(fitness, u, v) > s.minimumFitness;
193 }
194 if (erase) {
195 ++removed;
196 } else {
197 auto appended = next.appendPointFrom(input, static_cast<std::size_t>(i));
198 if (!appended.ok()) return Result<int>::failure(appended.status());
199 }
200 }
201 static_assert(std::is_nothrow_move_assignable_v<PointSet>);
202 output = std::move(next);
204}
205
207 using namespace raster_detail;
208 const auto orderedPositive = [](float minimum, float maximum) {
209 return std::isfinite(minimum) && minimum > 0 && std::isfinite(maximum) && maximum >= minimum;
210 };
211 if (!validScaleMode(s.scaleMode) || !orderedPositive(s.previousMinimumWidth, s.previousMaximumWidth) ||
212 !orderedPositive(s.previousMinimumHeight, s.previousMaximumHeight) ||
213 !orderedPositive(s.minimumWidth, s.maximumWidth) || !orderedPositive(s.minimumHeight, s.maximumHeight) ||
214 !std::isfinite(s.bendFactor) || s.bendFactor < 0 || !std::isfinite(s.boundsRadius) ||
215 s.boundsRadius <= 0 || s.asset.empty())
216 return invalid("terrain.rescaleTreePoints: valid previous and replacement prototype required");
217 PointSet next = input;
218 int matched = 0;
219 for (int i = 0; i < next.getCount(); ++i) {
220 if (next.getStringAttribute(i, "asset", "") != s.asset) continue;
221 auto& point = next.mutablePoint(static_cast<std::size_t>(i));
222 double width = s.minimumWidth, height = s.minimumHeight;
223 if (s.scaleMode != TerrainTreeScaleMode::Fixed) {
224 const auto inverse = [](double value, double minimum, double maximum) {
225 if (minimum == maximum) return 0.0;
226 return std::clamp((value - minimum) / (maximum - minimum), 0.0, 1.0);
227 };
228 width = std::lerp(double(s.minimumWidth), double(s.maximumWidth),
229 inverse(point.scaleX, s.previousMinimumWidth, s.previousMaximumWidth));
230 height = std::lerp(double(s.minimumHeight), double(s.maximumHeight),
231 inverse(point.scaleY, s.previousMinimumHeight, s.previousMaximumHeight));
232 }
233 const double radius = double(s.boundsRadius) * width;
235 return invalid("terrain.rescaleTreePoints: refreshed scale is not representable");
236 point.scaleX = point.scaleZ = float(width);
237 point.scaleY = float(height);
238 point.boundsMinX = point.boundsMinZ = float(-radius);
239 point.boundsMaxX = point.boundsMaxZ = float(radius);
240 point.boundsMaxY = float(height);
241 auto bend = next.trySetFloatAttribute(i, "bendFactor", s.bendFactor);
242 if (!bend.ok()) return Result<int>::failure(bend.status());
243 ++matched;
244 }
245 static_assert(std::is_nothrow_move_assignable_v<PointSet>);
246 output = std::move(next);
247 return Result<int>::success(matched);
248}
249
251 const std::vector<TerrainTreeTile>& tiles, const Heightmap& operationFitness,
252 const TerrainTreePlacementSettings& s, const TerrainStampSettings& operationSettings,
253 TerrainTreeOperationMode mode, bool worldMapOperation, const std::vector<std::string>& validTerrainNames) {
254 using namespace raster_detail;
255 const bool validMode = mode == TerrainTreeOperationMode::Add || mode == TerrainTreeOperationMode::Replace ||
257 if (tiles.empty() || !validMode || !validRaster(operationFitness) || !std::isfinite(s.heightScale) ||
258 !std::isfinite(s.spacing) || s.spacing <= 0 || !std::isfinite(s.spawnDensity) || s.spawnDensity <= 0 ||
259 !normalized(s.jitterPercent) || !normalized(s.failureRate) || !normalized(s.minimumFitness) ||
260 !std::isfinite(s.seaLevel) || !std::isfinite(s.customOffset) || !std::isfinite(s.minimumYOffset) ||
261 !std::isfinite(s.maximumYOffset) || s.maximumYOffset < s.minimumYOffset || !validScaleMode(s.scaleMode) ||
262 !validYOffsetMode(s.yOffsetMode) || !std::isfinite(s.minimumWidth) || s.minimumWidth <= 0 ||
263 !std::isfinite(s.maximumWidth) || s.maximumWidth < s.minimumWidth || !std::isfinite(s.minimumHeight) ||
264 s.minimumHeight <= 0 || !std::isfinite(s.maximumHeight) || s.maximumHeight < s.minimumHeight ||
265 !normalized(s.widthRandomPercentage) || !normalized(s.heightRandomPercentage) || !normalized(s.healthyR) ||
266 !normalized(s.healthyG) || !normalized(s.healthyB) || !normalized(s.healthyA) || !normalized(s.dryR) ||
267 !normalized(s.dryG) || !normalized(s.dryB) || !normalized(s.dryA) || !std::isfinite(s.bendFactor) ||
268 s.bendFactor < 0 || !std::isfinite(s.boundsRadius) || s.boundsRadius <= 0 || s.namespaceId == 0 ||
269 s.asset.empty() || s.maxPoints < 0)
271 DiagnosticCode::InvalidArgument, "terrain.tree.multitile: valid tiles, operation and prototype required"));
272 for (float value : operationFitness.data())
273 if (!normalized(value))
275 DiagnosticCode::InvalidArgument, "terrain.tree.multitile: normalized operation fitness required"));
276
277 std::vector<TerrainOperationTile> operationTiles;
278 std::unordered_set<PointSet*> owners;
279 operationTiles.reserve(tiles.size());
280 for (const auto& tile : tiles) {
281 if (!tile.trees || !tile.heights || !owners.insert(tile.trees).second || !validRaster(*tile.heights))
283 DiagnosticCode::InvalidArgument, "terrain.tree.multitile: distinct outputs and finite heights required"));
284 operationTiles.push_back({tile.name, tile.originX, tile.originZ, tile.width, tile.depth,
285 tile.treeResolutionX, tile.treeResolutionY, tile.worldMap});
286 }
287 auto mapped = mapTerrainOperationMultiTile(operationTiles, operationSettings, TerrainOperationDomain::Tree,
288 worldMapOperation, validTerrainNames);
289 if (!mapped.ok()) return mapped;
290 auto report = std::move(mapped.value());
291 if (operationFitness.getWidth() != report.operationWidth ||
292 operationFitness.getHeight() != report.operationHeight)
294 DiagnosticCode::InvalidArgument, "terrain.tree.multitile: operation fitness dimensions must match window"));
295 const double increment = double(s.spacing) / s.spawnDensity;
296 if (!isRepresentable(increment) || increment <= 0)
298 DiagnosticCode::InvalidArgument, "terrain.tree.multitile: effective spacing is not representable"));
299
300 struct Candidate { PointSet* target; PointSet value; };
301 std::vector<Candidate> candidates;
302 candidates.reserve(report.mappings.size());
303 PcgXorshiftPlus random(s.seed);
304 int emitted = 0;
305 for (const auto& mapping : report.mappings) {
306 const auto tile = std::find_if(tiles.begin(), tiles.end(),
307 [&](const auto& item) { return item.name == mapping.terrainName; });
308 if (tile == tiles.end())
310 DiagnosticCode::InvariantViolation, "terrain.tree.multitile: mapped terrain was not supplied"));
311 PointSet next = *tile->trees;
312 int changed = 0;
314 PointSet kept;
315 kept.reserve(size_t(next.getCount()));
316 for (int i = 0; i < next.getCount(); ++i) {
317 const auto& point = next.points()[size_t(i)];
318 bool remove = false;
319 if (next.getStringAttribute(i, "asset", "") == s.asset) {
320 const int localX = nearestEven((double(point.x) - tile->originX) * tile->treeResolutionX / tile->width);
321 const int localY = nearestEven((double(point.z) - tile->originZ) * tile->treeResolutionY / tile->depth);
322 if (localX >= mapping.localX && localX < mapping.localX + mapping.width &&
323 localY >= mapping.localY && localY < mapping.localY + mapping.height) {
324 const int ox = mapping.operationX + localX - mapping.localX;
325 const int oy = mapping.operationY + localY - mapping.localY;
326 remove = operationFitness.height(ox, oy) > s.minimumFitness;
327 }
328 }
329 if (remove) ++changed;
330 else {
331 auto appended = kept.appendPointFrom(next, size_t(i));
332 if (!appended.ok()) return Result<TerrainMultiTileReport>::failure(appended.status());
333 }
334 }
335 next = std::move(kept);
336 } else {
337 const double startX = mapping.localX * tile->width / tile->treeResolutionX;
338 const double startZ = mapping.localY * tile->depth / tile->treeResolutionY;
339 const double stopX = (mapping.localX + mapping.width) * tile->width / tile->treeResolutionX + increment;
340 const double stopZ = (mapping.localY + mapping.height) * tile->depth / tile->treeResolutionY + increment;
341 const double jitter = increment * s.jitterPercent;
342 std::uint64_t ordinal = 0;
343 for (double x = startX; x <= stopX; x += increment)
344 for (double z = startZ; z <= stopZ; z += increment, ++ordinal) {
345 if (random.next() < s.failureRate) continue;
346 const double xPos = x + random.next(float(-jitter), float(jitter));
347 const double zPos = z + random.next(float(-jitter), float(jitter));
348 const int localX = nearestEven(xPos * tile->treeResolutionX / tile->width);
349 const int localY = nearestEven(zPos * tile->treeResolutionY / tile->depth);
350 if (localX < mapping.localX || localX >= mapping.localX + mapping.width ||
351 localY < mapping.localY || localY >= mapping.localY + mapping.height) continue;
352 const float strength = operationFitness.height(mapping.operationX + localX - mapping.localX,
353 mapping.operationY + localY - mapping.localY);
354 if (random.next(s.minimumFitness, 1) > strength) continue;
355 if (emitted >= s.maxPoints)
357 DiagnosticCode::InvalidArgument, "terrain.tree.multitile: point budget exceeded"));
358 const double u = xPos / tile->width, v = zPos / tile->depth;
360 point.id = mix(mix(s.namespaceId) ^ mix(stableNameHash(tile->name)) ^ mix(ordinal + 1));
361 if (!point.id) point.id = 1;
362 double worldY = sample(*tile->heights, u, v) * s.heightScale;
363 if (!s.snapToTerrain) {
364 if (s.yOffsetMode == TerrainTreeYOffsetMode::SeaLevel) worldY = s.seaLevel;
365 if (s.yOffsetMode == TerrainTreeYOffsetMode::Custom) worldY = s.customOffset;
366 worldY += random.next(s.minimumYOffset, s.maximumYOffset);
367 }
368 double width = s.minimumWidth, height = s.minimumHeight;
370 width = std::lerp(double(s.minimumWidth), double(s.maximumWidth), strength);
371 height = std::lerp(double(s.minimumHeight), double(s.maximumHeight), strength);
372 } else if (s.scaleMode == TerrainTreeScaleMode::Random) {
373 const double value = random.next();
374 width = std::lerp(double(s.minimumWidth), double(s.maximumWidth), value);
375 height = std::lerp(double(s.minimumHeight), double(s.maximumHeight), value);
376 }
378 const double value = random.next();
379 width *= std::lerp(1.0 - s.widthRandomPercentage, 1.0 + s.widthRandomPercentage, value);
380 height *= std::lerp(1.0 - s.heightRandomPercentage, 1.0 + s.heightRandomPercentage, value);
381 }
382 const double worldX = tile->originX + xPos, worldZ = tile->originZ + zPos;
383 const double radius = double(s.boundsRadius) * width;
384 if (!isRepresentable(worldX) || !isRepresentable(worldY) || !isRepresentable(worldZ) ||
388 "terrain.tree.multitile: generated transform is not representable"));
389 point.x = float(worldX); point.y = float(worldY); point.z = float(worldZ);
390 point.yaw = random.next(0, 360); point.scaleX = point.scaleZ = float(width); point.scaleY = float(height);
391 point.density = strength; point.colorR = std::lerp(s.dryR, s.healthyR, strength);
392 point.colorG = std::lerp(s.dryG, s.healthyG, strength); point.colorB = std::lerp(s.dryB, s.healthyB, strength);
393 point.colorA = std::lerp(s.dryA, s.healthyA, strength);
394 point.seed = uint32_t(mix(point.id ^ uint32_t(s.seed)));
395 point.boundsMinX = point.boundsMinZ = -float(radius);
396 point.boundsMaxX = point.boundsMaxZ = float(radius);
397 point.boundsMaxY = float(height);
398 const int row = next.appendPoint(point);
399 auto asset = next.trySetStringAttribute(row, "asset", s.asset);
400 if (!asset.ok()) return Result<TerrainMultiTileReport>::failure(asset.status());
401 auto source = next.trySetStringAttribute(row, "spawnNamespace", std::to_string(s.namespaceId));
402 if (!source.ok()) return Result<TerrainMultiTileReport>::failure(source.status());
403 const auto candidateIdentity = static_cast<std::int64_t>(
404 mix(stableNameHash(tile->name)) ^ mix(ordinal + 1));
405 auto candidate = next.trySetIntAttribute(row, "treeCandidate", candidateIdentity);
406 if (!candidate.ok()) return Result<TerrainMultiTileReport>::failure(candidate.status());
407 auto bend = next.trySetFloatAttribute(row, "bendFactor", s.bendFactor);
408 if (!bend.ok()) return Result<TerrainMultiTileReport>::failure(bend.status());
409 ++emitted;
410 ++changed;
411 }
412 }
413 if (report.changedSamples > std::numeric_limits<int>::max() - changed)
415 DiagnosticCode::InvalidArgument, "terrain.tree.multitile: changed point count exceeds integer range"));
416 report.changedSamples += changed;
417 candidates.push_back({tile->trees, std::move(next)});
418 }
419 for (auto& candidate : candidates) *candidate.target = std::move(candidate.value);
420 return Result<TerrainMultiTileReport>::success(std::move(report));
421}
422
424 struct Tile {
425 std::string name;
428 double originX = 0, originZ = 0, width = 1, depth = 1;
430 bool worldMap = false;
431 };
432 struct Snapshot {
433 std::vector<PointSet> trees;
435 };
436 std::vector<Tile> tiles;
438 std::vector<Snapshot> history;
439 int cursor = 0;
441 Snapshot result;
442 result.trees.reserve(tiles.size());
443 for (const auto& tile : tiles) result.trees.push_back(tile.trees);
444 result.report = last;
445 return result;
446 }
447 void restore(const Snapshot& snapshot) {
448 for (size_t i = 0; i < tiles.size(); ++i) tiles[i].trees = snapshot.trees[i];
450 }
451};
452
453namespace {
454Result<int> invalidTreeWorkspace(const char* message) {
456}
457} // namespace
458
463
464Result<int> TerrainMultiTreeWorkspace::addTile(const std::string& name, const PointSet& trees,
465 const Heightmap& heights, double originX, double originZ,
466 double width, double depth, int resolutionX, int resolutionY,
467 bool worldMap) {
468 if (!impl_ || name.empty() || !raster_detail::validRaster(heights))
469 return invalidTreeWorkspace("terrain.tree.workspace: initialized named tile required");
470 if (std::any_of(impl_->tiles.begin(), impl_->tiles.end(), [&](const auto& tile) { return tile.name == name; }))
471 return invalidTreeWorkspace("terrain.tree.workspace: duplicate tile name");
472 if (impl_->history.size() > 1)
473 return invalidTreeWorkspace("terrain.tree.workspace: topology is fixed after applying");
474 auto candidate = std::make_unique<Impl>(*impl_);
475 candidate->tiles.push_back({name, trees, heights, originX, originZ, width, depth, resolutionX, resolutionY,
476 worldMap});
477 std::vector<TerrainOperationTile> descriptors;
478 for (const auto& tile : candidate->tiles)
479 descriptors.push_back({tile.name, tile.originX, tile.originZ, tile.width, tile.depth, tile.resolutionX,
480 tile.resolutionY, tile.worldMap});
482 bounds.centerX = originX + width * 0.5;
483 bounds.centerZ = originZ + depth * 0.5;
484 bounds.width = width;
485 bounds.depth = depth;
486 auto checked = mapTerrainOperationMultiTile(descriptors, bounds, TerrainOperationDomain::Tree, worldMap, {name});
487 if (!checked.ok()) return Result<int>::failure(checked.status());
488 candidate->history.clear();
489 candidate->history.push_back(candidate->snapshot());
490 candidate->cursor = 0;
491 impl_.swap(candidate);
492 return Result<int>::success(int(impl_->tiles.size()));
493}
494
495Result<int> TerrainMultiTreeWorkspace::apply(const Heightmap& operationFitness,
497 const TerrainStampSettings& operationSettings,
498 TerrainTreeOperationMode mode, bool worldMapOperation) {
499 if (!impl_) return invalidTreeWorkspace("terrain.tree.workspace: moved-from workspace");
500 auto candidate = std::make_unique<Impl>(*impl_);
501 std::vector<TerrainTreeTile> descriptors;
502 for (auto& tile : candidate->tiles)
503 descriptors.push_back({tile.name, &tile.trees, &tile.heights, tile.originX, tile.originZ, tile.width,
504 tile.depth, tile.resolutionX, tile.resolutionY, tile.worldMap});
505 auto applied = applyTerrainTreesMultiTile(descriptors, operationFitness, settings, operationSettings, mode,
506 worldMapOperation);
507 if (!applied.ok()) return Result<int>::failure(applied.status());
508 candidate->last = std::move(applied.value());
509 candidate->history.resize(size_t(candidate->cursor + 1));
510 candidate->history.push_back(candidate->snapshot());
511 ++candidate->cursor;
512 const int changed = candidate->last.changedSamples;
513 impl_.swap(candidate);
514 return Result<int>::success(changed);
515}
516
517Result<int> TerrainMultiTreeWorkspace::copyTile(const std::string& name, PointSet& output) const {
518 if (!impl_) return invalidTreeWorkspace("terrain.tree.workspace: moved-from workspace");
519 const auto found = std::find_if(impl_->tiles.begin(), impl_->tiles.end(),
520 [&](const auto& tile) { return tile.name == name; });
521 if (found == impl_->tiles.end()) return invalidTreeWorkspace("terrain.tree.workspace: tile not found");
522 PointSet copy = found->trees;
523 output = std::move(copy);
524 return Result<int>::success(output.getCount());
525}
526Result<int> TerrainMultiTreeWorkspace::undo() {
527 if (!impl_ || impl_->cursor <= 0) return invalidTreeWorkspace("terrain.tree.workspace: no undo snapshot");
528 --impl_->cursor;
529 impl_->restore(impl_->history[size_t(impl_->cursor)]);
530 return Result<int>::success(impl_->cursor);
531}
532Result<int> TerrainMultiTreeWorkspace::redo() {
533 if (!impl_ || impl_->cursor + 1 >= int(impl_->history.size()))
534 return invalidTreeWorkspace("terrain.tree.workspace: no redo snapshot");
535 ++impl_->cursor;
536 impl_->restore(impl_->history[size_t(impl_->cursor)]);
537 return Result<int>::success(impl_->cursor);
538}
539int TerrainMultiTreeWorkspace::getTileCount() const noexcept { return impl_ ? int(impl_->tiles.size()) : 0; }
540int TerrainMultiTreeWorkspace::getLastChangedSamples() const noexcept {
541 return impl_ ? impl_->last.changedSamples : 0;
542}
543int TerrainMultiTreeWorkspace::getLastAffectedTiles() const noexcept {
544 return impl_ ? impl_->last.affectedTiles : 0;
545}
546int TerrainMultiTreeWorkspace::getOperationCount() const noexcept {
547 return impl_ ? std::max(0, int(impl_->history.size()) - 1) : 0;
548}
549int TerrainMultiTreeWorkspace::getAppliedCount() const noexcept { return impl_ ? impl_->cursor : 0; }
550} // namespace eve::procgen
LogicalId target
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::string output
const std::string & s
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
float maximum[3]
float minimum[3]
float u
Definition Grass.cpp:233
float v
std::uint32_t height
std::uint32_t width
std::string name
float radius
std::uint32_t seed
Definition PointSet.cpp:807
bool found
int removed
Cell cell
Battle::Random random
TerrainThermalSettings settings
const UnitySourceAsset & source
std::uint32_t depth
double oy
double ox
glm::vec3 point
std::vector< WfcTile > tiles
Definition WfcSimple.cpp:22
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
In-memory terrain heightmap: a dense float grid (row-major, index = y * width + x) materialized from ...
Definition Heightmap.h:21
int getHeight() const
Returns the height.
Definition Heightmap.cpp:17
const std::vector< float > & data() const
Data.
Definition Heightmap.h:48
float height(int x, int y) const
Height.
Definition Heightmap.cpp:28
int getWidth() const
Returns the width.
Definition Heightmap.cpp:16
Script-friendly collection of attributed 3D samples.
Definition PointSet.h:59
Result< int > appendPointFrom(const PointSet &source, std::size_t sourceIndex)
Append a point and its attributes from another set.
Definition PointSet.cpp:69
void reserve(std::size_t count)
Reserve point storage without changing point or attribute row counts.
Definition PointSet.cpp:60
Owning script-safe multi-terrain tree transaction workspace. Tree sets and height rasters are copied ...
~TerrainMultiTreeWorkspace()
Terrain multi tree workspace.
TerrainMultiTreeWorkspace()
Terrain multi tree workspace.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
double textureSample(const Heightmap &map, double u, double v)
Texture sample.
double sample(const Heightmap &map, double u, double v)
Sample.
bool validRaster(const Heightmap &map)
Valid raster.
Result< int > invalid(std::string message)
Invalid.
bool isRepresentable(double value)
True when representable.
Result< int > removeTerrainTreePoints(PointSet &output, const PointSet &input, const Heightmap &fitness, const TerrainTreePlacementSettings &s)
Remove matching tree points wherever the current fitness is strictly above the threshold.
Result< int > rescaleTerrainTreePoints(PointSet &output, const PointSet &input, const TerrainTreeRescaleSettings &s)
Atomically refresh scale, bounds and bend metadata for one tree asset while preserving identity/trans...
Result< TerrainMultiTileReport > mapTerrainOperationMultiTile(const std::vector< TerrainOperationTile > &tiles, const TerrainStampSettings &settings, TerrainOperationDomain domain, bool worldMapOperation, const std::vector< std::string > &validTerrainNames)
Calculate Pcg-compatible local and shared pixel rectangles for any multi-terrain raster domain.
Result< TerrainMultiTileReport > applyTerrainTreesMultiTile(const std::vector< TerrainTreeTile > &tiles, const Heightmap &operationFitness, const TerrainTreePlacementSettings &s, const TerrainStampSettings &operationSettings, TerrainTreeOperationMode mode, bool worldMapOperation, const std::vector< std::string > &validTerrainNames)
Apply one Pcg tree distribution window across selected terrain tiles atomically.
TerrainTreeScaleMode
Pcg-compatible rules for deriving tree scale from placement fitness.
TerrainTreeYOffsetMode
Height reference used when a tree does not snap directly to terrain.
Result< int > exportTerrainTreePoints(PointSet &output, const Heightmap &fitness, const Heightmap &heights, const TerrainTreePlacementSettings &s)
Scan a terrain rectangle and atomically export accepted Pcg-style tree instances.
TerrainTreeOperationMode
Pcg SetTerrainTrees operation branch. Add and Replace both append after upstream clearing policy.
One deterministic sample used by script-first procedural pipelines.
Definition PointSet.h:17
std::uint64_t id
Stable non-zero identity; zero marks legacy or not-yet-assigned data.
Definition PointSet.h:19
Completed multi-tile stamp statistics and Pcg-compatible affected-pixel mappings.
Value configuration for a rectangular stamp in world X/Z coordinates.
Explicit deterministic tree placement configuration for one terrain rectangle and resource.
Previous and replacement prototype values used for atomic Pcg-style tree refresh.
glm::vec4 bounds