载入中...
搜索中...
未找到
TerrainObjectPlacement.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"
12
13namespace eve::procgen {
14namespace {
15class PcgRandom {
16public:
17 explicit PcgRandom(std::int32_t seed) {
18 const auto value = seed == 0 ? 1U : static_cast<std::uint32_t>(seed);
19 a_ = UINT64_C(181353) * value; b_ = UINT64_C(7) * value;
20 }
21 float next() {
22 auto x = a_, y = b_; a_ = y; x ^= x << 23; x ^= x >> 17; x ^= y ^ (y >> 26); b_ = x;
23 return static_cast<float>(x + y) / static_cast<float>(std::numeric_limits<std::uint64_t>::max());
24 }
25 float next(float minimum, float maximum) { return minimum + next() * (maximum - minimum); }
26 int next(int minimum, int maximum) {
27 if (minimum == maximum) return minimum;
28 return static_cast<int>(next(float(minimum), float(maximum) + 0.999F));
29 }
30private:
31 std::uint64_t a_ = 0, b_ = 0;
32};
33std::uint64_t mix(std::uint64_t value) {
34 value = (value ^ (value >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
35 value = (value ^ (value >> 27)) * UINT64_C(0x94d049bb133111eb);
36 return value ^ (value >> 31);
37}
38bool normalized(float value) { return std::isfinite(value) && value >= 0 && value <= 1; }
39bool ordered(float minimum, float maximum) { return std::isfinite(minimum) && std::isfinite(maximum) && maximum >= minimum; }
40bool positive(float value) { return std::isfinite(value) && value > 0; }
41int nearestEven(double value) { return static_cast<int>(std::nearbyint(value)); }
42bool validScale(TerrainObjectScaleMode mode) {
44}
45bool validYOffset(TerrainObjectYOffsetMode mode) {
47}
48struct Normal { double x = 0, y = 1, z = 0; };
49Normal normalAt(const Heightmap& heights, double u, double v, double width, double depth, double heightScale) {
50 const double du = heights.getWidth() > 1 ? 1.0 / (heights.getWidth() - 1) : 1;
51 const double dv = heights.getHeight() > 1 ? 1.0 / (heights.getHeight() - 1) : 1;
52 const double dx = (raster_detail::sample(heights, std::min(1.0, u + du), v) -
53 raster_detail::sample(heights, std::max(0.0, u - du), v)) * heightScale;
54 const double dz = (raster_detail::sample(heights, u, std::min(1.0, v + dv)) -
55 raster_detail::sample(heights, u, std::max(0.0, v - dv))) * heightScale;
56 const double sx = std::max(du * width * 2, std::numeric_limits<double>::epsilon());
57 const double sz = std::max(dv * depth * 2, std::numeric_limits<double>::epsilon());
58 Normal n{-dx / sx, 1, -dz / sz};
59 const double length = std::sqrt(n.x * n.x + n.y * n.y + n.z * n.z);
60 n.x /= length; n.y /= length; n.z /= length; return n;
61}
62Result<int> invalid(const char* message) {
64}
65bool validInstance(const TerrainObjectInstanceSettings& i) {
66 const auto scales = ordered(i.minimumScale, i.maximumScale) && positive(i.minimumScale) &&
67 ordered(i.minimumScaleX, i.maximumScaleX) && positive(i.minimumScaleX) &&
68 ordered(i.minimumScaleY, i.maximumScaleY) && positive(i.minimumScaleY) &&
69 ordered(i.minimumScaleZ, i.maximumScaleZ) && positive(i.minimumScaleZ);
70 return !i.asset.empty() && i.minimumInstances >= 0 && i.maximumInstances >= i.minimumInstances &&
71 normalized(i.failureRate) && ordered(i.minimumOffsetX, i.maximumOffsetX) &&
72 ordered(i.minimumOffsetY, i.maximumOffsetY) && ordered(i.minimumOffsetZ, i.maximumOffsetZ) &&
73 validYOffset(i.yOffsetMode) && std::isfinite(i.customOffset) && validScale(i.scaleMode) && scales &&
74 normalized(i.scaleRandomPercentage) && normalized(i.scaleRandomPercentageX) &&
75 normalized(i.scaleRandomPercentageY) && normalized(i.scaleRandomPercentageZ) &&
76 ordered(i.minimumRotationX, i.maximumRotationX) && ordered(i.minimumRotationY, i.maximumRotationY) &&
77 ordered(i.minimumRotationZ, i.maximumRotationZ);
78}
79std::uint64_t stableNameHash(const std::string& value) {
80 std::uint64_t hash = UINT64_C(1469598103934665603);
81 for (const unsigned char ch : value) { hash ^= ch; hash *= UINT64_C(1099511628211); }
82 return hash;
83}
84} // namespace
85
87 if (!validInstance(instance)) return invalid("terrain.objectSettings: valid resource instance required");
88 instances.push_back(instance);
89 return Result<int>::success(static_cast<int>(instances.size()));
90}
91
94 using namespace raster_detail;
95 if (!validRaster(fitness) || !validRaster(heights) || !std::isfinite(s.originX) || !std::isfinite(s.originZ) ||
96 !positive(s.width) || !positive(s.depth) || !std::isfinite(s.heightScale) || !positive(s.spacing) ||
97 !positive(s.spawnDensity) || !normalized(s.jitterPercent) || !normalized(s.failureRate) ||
98 !normalized(s.minimumFitness) || !normalized(s.minimumInstanceFitness) ||
99 !ordered(s.minimumDirection, s.maximumDirection) || !positive(s.boundsRadius) ||
100 !std::isfinite(s.boundsCheckQuality) || s.boundsCheckQuality < 0 || s.boundsCheckQuality > 100 ||
101 !positive(s.prototypeScale) || !std::isfinite(s.startOffsetX) || !std::isfinite(s.startOffsetZ) ||
102 !std::isfinite(s.seaLevel) || s.namespaceId == 0 || s.maxPoints < 0 || s.prototype.empty() ||
103 s.instances.empty() || !std::all_of(s.instances.begin(), s.instances.end(), validInstance))
104 return invalid("terrain.objectPoints: valid rasters, prototype, instances, probabilities and transforms required");
105 if (!std::all_of(fitness.data().begin(), fitness.data().end(), normalized))
106 return invalid("terrain.objectPoints: normalized fitness required");
107 const double increment = double(s.spacing) / s.spawnDensity;
108 if (!isRepresentable(increment) || increment <= 0) return invalid("terrain.objectPoints: spacing is not representable");
109 PointSet next;
110 PcgRandom random(s.seed);
111 std::vector<std::pair<double, double>> acceptedCenters;
112 int emitted = 0;
113 std::uint64_t candidate = 0;
114 const double jitter = double(s.spacing) * s.jitterPercent;
115 for (double x = s.startOffsetX; x <= double(s.width) + increment; x += increment) {
116 for (double z = s.startOffsetZ; z <= double(s.depth) + increment; z += increment, ++candidate) {
117 if (random.next() < s.failureRate) continue;
118 const double localX = x + random.next(float(-jitter), float(jitter)) * 0.5;
119 const double localZ = z + random.next(float(-jitter), float(jitter)) * 0.5;
120 if (localX < 0 || localZ < 0 || localX > s.width || localZ > s.depth) continue;
121 const double u = localX / s.width, v = localZ / s.depth;
122 const double strength = textureSample(fitness, u, v);
123 if (random.next(s.minimumFitness, 1.0F) > strength) continue;
124 const double centerX = s.originX + localX, centerZ = s.originZ + localZ;
125 if (s.boundsCollisionCheck && std::any_of(acceptedCenters.begin(), acceptedCenters.end(), [&](const auto& p) {
126 return std::hypot(centerX - p.first, centerZ - p.second) < double(s.boundsRadius) * 2;
127 })) continue;
128 const int rx = std::max(0, int(std::round(s.boundsRadius * (fitness.getWidth() - 1) / s.width)));
129 const int rz = std::max(0, int(std::round(s.boundsRadius * (fitness.getHeight() - 1) / s.depth)));
130 const int cx = int(std::round(u * (fitness.getWidth() - 1))), cz = int(std::round(v * (fitness.getHeight() - 1)));
131 const int stepX = std::max(1, int(std::ceil(2 * rx * (1 - s.boundsCheckQuality / 100.0))));
132 const int stepZ = std::max(1, int(std::ceil(2 * rz * (1 - s.boundsCheckQuality / 100.0))));
133 double sum = 0; int checks = 0;
134 for (int px = cx - rx; px <= cx + rx; px += stepX)
135 for (int pz = cz - rz; pz <= cz + rz; pz += stepZ) {
136 if (px >= 0 && pz >= 0 && px < fitness.getWidth() && pz < fitness.getHeight()) sum += fitness.height(px, pz);
137 ++checks;
138 }
139 if (sum / std::max(1, checks) < s.minimumFitness) continue;
140 const float direction = random.next(s.minimumDirection, s.maximumDirection);
141 acceptedCenters.emplace_back(centerX, centerZ);
142 for (std::size_t resource = 0; resource < s.instances.size(); ++resource) {
143 const auto& instance = s.instances[resource];
144 const int count = random.next(instance.minimumInstances, instance.maximumInstances);
145 for (int ordinal = 0; ordinal < count; ++ordinal) {
146 if (random.next() < instance.failureRate) continue;
147 const double offsetX = random.next(instance.minimumOffsetX, instance.maximumOffsetX) * s.prototypeScale;
148 const double offsetZ = random.next(instance.minimumOffsetZ, instance.maximumOffsetZ) * s.prototypeScale;
149 const double radians = direction * 3.14159265358979323846 / 180.0;
150 const double ix = centerX + std::cos(radians) * offsetX - std::sin(radians) * offsetZ;
151 const double iz = centerZ + std::sin(radians) * offsetX + std::cos(radians) * offsetZ;
152 const double iu = (ix - s.originX) / s.width, iv = (iz - s.originZ) / s.depth;
153 if (iu < 0 || iv < 0 || iu > 1 || iv > 1) continue;
154 const double instanceStrength = textureSample(fitness, iu, iv);
155 if (instanceStrength < s.minimumInstanceFitness) continue;
156 if (emitted >= s.maxPoints) return invalid("terrain.objectPoints: point budget exceeded");
157 const Normal normal = normalAt(heights, iu, iv, s.width, s.depth, s.heightScale);
158 double iy = sample(heights, iu, iv) * s.heightScale;
159 if (instance.yOffsetMode == TerrainObjectYOffsetMode::SeaLevel) iy = s.seaLevel;
160 if (instance.yOffsetMode == TerrainObjectYOffsetMode::Custom) iy = instance.customOffset;
161 double sx = instance.minimumScaleX, sy = instance.minimumScaleY, sz = instance.minimumScaleZ;
162 if (instance.commonScale) sx = sy = sz = instance.minimumScale;
163 if (instance.scaleMode == TerrainObjectScaleMode::Random) {
164 if (instance.commonScale) sx = sy = sz = random.next(instance.minimumScale, instance.maximumScale);
165 else { sx = random.next(instance.minimumScaleX, instance.maximumScaleX); sy = random.next(instance.minimumScaleY, instance.maximumScaleY); sz = random.next(instance.minimumScaleZ, instance.maximumScaleZ); }
166 } else if (instance.scaleMode == TerrainObjectScaleMode::Fitness || instance.scaleMode == TerrainObjectScaleMode::FitnessRandomized) {
167 if (instance.commonScale) sx = sy = sz = std::lerp(instance.minimumScale, instance.maximumScale, instanceStrength);
168 else { sx = std::lerp(instance.minimumScaleX, instance.maximumScaleX, instanceStrength); sy = std::lerp(instance.minimumScaleY, instance.maximumScaleY, instanceStrength); sz = std::lerp(instance.minimumScaleZ, instance.maximumScaleZ, instanceStrength); }
169 }
170 if (instance.scaleMode == TerrainObjectScaleMode::FitnessRandomized) {
171 if (instance.commonScale) { const double factor = random.next(1 - instance.scaleRandomPercentage, 1 + instance.scaleRandomPercentage); sx *= factor; sy *= factor; sz *= factor; }
172 else { sx *= random.next(1 - instance.scaleRandomPercentageX, 1 + instance.scaleRandomPercentageX); sy *= random.next(1 - instance.scaleRandomPercentageY, 1 + instance.scaleRandomPercentageY); sz *= random.next(1 - instance.scaleRandomPercentageZ, 1 + instance.scaleRandomPercentageZ); }
173 }
174 sx *= s.prototypeScale; sy *= s.prototypeScale; sz *= s.prototypeScale;
175 const double yOffset = random.next(instance.minimumOffsetY, instance.maximumOffsetY) * sy;
176 double finalX = ix, finalY = iy, finalZ = iz;
177 if (instance.yOffsetAlongSlope) { finalX += normal.x * yOffset; finalY += normal.y * yOffset; finalZ += normal.z * yOffset; }
178 else finalY += yOffset;
179 double pitch = random.next(instance.minimumRotationX, instance.maximumRotationX);
180 double yaw = random.next(instance.minimumRotationY + direction, instance.maximumRotationY + direction);
181 double roll = random.next(instance.minimumRotationZ, instance.maximumRotationZ);
182 if (instance.alignForwardToSlope) yaw += std::atan2(normal.x, normal.z) * 180.0 / 3.14159265358979323846;
183 if (instance.rotateToSlope) { pitch += std::atan2(-normal.z, normal.y) * 180.0 / 3.14159265358979323846; roll += std::atan2(normal.x, normal.y) * 180.0 / 3.14159265358979323846; }
184 const double radius = s.boundsRadius * std::max(sx, sz);
185 if (!isRepresentable(finalX) || !isRepresentable(finalY) || !isRepresentable(finalZ) ||
186 !isRepresentable(sx) || sx <= 0 || !isRepresentable(sy) || sy <= 0 ||
187 !isRepresentable(sz) || sz <= 0 || !isRepresentable(radius) || radius <= 0 ||
189 return invalid("terrain.objectPoints: generated transform is not representable");
191 point.id = mix(mix(mix(mix(s.namespaceId) ^ (candidate + 1)) ^ (resource + 1)) ^
192 (std::uint64_t(ordinal) + 1));
193 if (!point.id) return invalid("terrain.objectPoints: generated identity is reserved");
194 point.x = float(finalX); point.y = float(finalY); point.z = float(finalZ);
195 point.normalX = float(normal.x); point.normalY = float(normal.y); point.normalZ = float(normal.z);
196 point.pitch = float(pitch); point.yaw = float(yaw); point.roll = float(roll);
197 point.scaleX = float(sx); point.scaleY = float(sy); point.scaleZ = float(sz); point.density = float(instanceStrength);
198 point.boundsMinX = point.boundsMinZ = -float(radius); point.boundsMaxX = point.boundsMaxZ = float(radius); point.boundsMaxY = float(sy);
199 point.seed = static_cast<std::uint32_t>(mix(point.id ^ static_cast<std::uint32_t>(s.seed)));
200 const int row = next.appendPoint(point);
201 auto asset = next.trySetStringAttribute(row, "asset", instance.asset); if (!asset.ok()) return Result<int>::failure(asset.status());
202 auto prototype = next.trySetStringAttribute(row, "objectPrototype", s.prototype); if (!prototype.ok()) return Result<int>::failure(prototype.status());
203 auto source = next.trySetStringAttribute(row, "spawnNamespace", std::to_string(s.namespaceId)); if (!source.ok()) return Result<int>::failure(source.status());
204 auto candidateAttr = next.trySetIntAttribute(row, "objectCandidate", static_cast<std::int64_t>(candidate)); if (!candidateAttr.ok()) return Result<int>::failure(candidateAttr.status());
205 auto resourceAttr = next.trySetIntAttribute(row, "objectResource", static_cast<std::int64_t>(resource)); if (!resourceAttr.ok()) return Result<int>::failure(resourceAttr.status());
206 ++emitted;
207 }
208 }
209 }
210 }
211 static_assert(std::is_nothrow_move_assignable_v<PointSet>);
212 output = std::move(next);
213 return Result<int>::success(emitted);
214}
215
217 const TerrainObjectPlacementSettings& s, float removalStrength) {
218 using namespace raster_detail;
219 if (!validRaster(fitness) || !std::all_of(fitness.data().begin(), fitness.data().end(), normalized) ||
220 !std::isfinite(s.originX) || !std::isfinite(s.originZ) || !positive(s.width) || !positive(s.depth) ||
221 s.prototype.empty() || !normalized(removalStrength))
222 return invalid("terrain.removeObjectPoints: valid fitness, domain, prototype and threshold required");
223 PointSet next; next.reserve(static_cast<std::size_t>(input.getCount())); int removed = 0;
224 for (int i = 0; i < input.getCount(); ++i) {
225 const auto& point = input.points()[static_cast<std::size_t>(i)];
226 const double u = (point.x - s.originX) / s.width, v = (point.z - s.originZ) / s.depth;
227 const bool erase = input.getStringAttribute(i, "objectPrototype", "") == s.prototype &&
228 u >= 0 && u <= 1 && v >= 0 && v <= 1 && textureSample(fitness, u, v) > removalStrength;
229 if (erase) ++removed;
230 else { auto copied = next.appendPointFrom(input, static_cast<std::size_t>(i)); if (!copied.ok()) return Result<int>::failure(copied.status()); }
231 }
232 output = std::move(next); return Result<int>::success(removed);
233}
234
236 const std::vector<TerrainObjectTile>& tiles, const Heightmap& fitness,
237 const TerrainObjectPlacementSettings& s, const TerrainStampSettings& operationSettings,
238 TerrainObjectOperationMode mode, bool worldMapOperation, const std::vector<std::string>& validTerrainNames) {
239 using namespace raster_detail;
240 const bool validMode = mode == TerrainObjectOperationMode::Add || mode == TerrainObjectOperationMode::Replace ||
242 if (tiles.empty() || !validMode || !validRaster(fitness) || !positive(s.spacing) ||
243 !positive(s.spawnDensity) || !normalized(s.jitterPercent) || !normalized(s.failureRate) ||
244 !normalized(s.minimumFitness) || !normalized(s.minimumInstanceFitness) ||
245 !ordered(s.minimumDirection, s.maximumDirection) || !positive(s.boundsRadius) ||
246 !std::isfinite(s.boundsCheckQuality) || s.boundsCheckQuality < 0 || s.boundsCheckQuality > 100 ||
247 !positive(s.prototypeScale) || !std::isfinite(s.startOffsetX) || !std::isfinite(s.startOffsetZ) ||
248 !std::isfinite(s.heightScale) || !std::isfinite(s.seaLevel) || s.namespaceId == 0 || s.maxPoints < 0 ||
249 s.prototype.empty() || s.instances.empty() || !std::all_of(s.instances.begin(), s.instances.end(), validInstance))
251 DiagnosticCode::InvalidArgument, "terrain.object.multitile: valid tiles, operation and prototype required"));
252 if (!std::all_of(fitness.data().begin(), fitness.data().end(), normalized))
254 DiagnosticCode::InvalidArgument, "terrain.object.multitile: normalized operation fitness required"));
255 std::vector<TerrainOperationTile> descriptors;
256 std::unordered_set<PointSet*> owners;
257 for (const auto& tile : tiles) {
258 if (!tile.objects || !tile.heights || !owners.insert(tile.objects).second || !validRaster(*tile.heights))
260 DiagnosticCode::InvalidArgument, "terrain.object.multitile: distinct outputs and finite heights required"));
261 descriptors.push_back({tile.name, tile.originX, tile.originZ, tile.width, tile.depth,
262 tile.objectResolutionX, tile.objectResolutionY, tile.worldMap});
263 }
264 auto mapped = mapTerrainOperationMultiTile(descriptors, operationSettings, TerrainOperationDomain::GameObject,
265 worldMapOperation, validTerrainNames);
266 if (!mapped.ok()) return mapped;
267 auto report = std::move(mapped.value());
268 if (fitness.getWidth() != report.operationWidth || fitness.getHeight() != report.operationHeight)
270 DiagnosticCode::InvalidArgument, "terrain.object.multitile: fitness dimensions must match operation window"));
271 const double increment = double(s.spacing) / s.spawnDensity;
272 if (!isRepresentable(increment) || increment <= 0)
274 DiagnosticCode::InvalidArgument, "terrain.object.multitile: effective spacing is not representable"));
275
276 struct Candidate { PointSet* target; PointSet value; };
277 std::vector<Candidate> candidates;
278 PcgRandom random(s.seed);
279 std::vector<std::pair<double, double>> acceptedCenters;
280 int emitted = 0;
281 std::uint64_t globalCandidate = 0;
282 for (const auto& mapping : report.mappings) {
283 const auto tile = std::find_if(tiles.begin(), tiles.end(), [&](const auto& item) {
284 return item.name == mapping.terrainName;
285 });
286 if (tile == tiles.end())
288 DiagnosticCode::InvariantViolation, "terrain.object.multitile: mapped terrain was not supplied"));
289 PointSet next;
290 int changed = 0;
291 const bool clearFirst = mode == TerrainObjectOperationMode::Replace || mode == TerrainObjectOperationMode::Remove;
292 if (clearFirst) {
293 next.reserve(static_cast<std::size_t>(tile->objects->getCount()));
294 for (int row = 0; row < tile->objects->getCount(); ++row) {
295 const auto& point = tile->objects->points()[static_cast<std::size_t>(row)];
296 const int lx = nearestEven((double(point.x) - tile->originX) * tile->objectResolutionX / tile->width);
297 const int ly = nearestEven((double(point.z) - tile->originZ) * tile->objectResolutionY / tile->depth);
298 bool erase = tile->objects->getStringAttribute(row, "objectPrototype", "") == s.prototype &&
299 lx >= mapping.localX && lx < mapping.localX + mapping.width &&
300 ly >= mapping.localY && ly < mapping.localY + mapping.height;
301 if (erase && mode == TerrainObjectOperationMode::Remove)
302 erase = fitness.height(mapping.operationX + lx - mapping.localX,
303 mapping.operationY + ly - mapping.localY) > s.minimumFitness;
304 if (erase) ++changed;
305 else {
306 auto copied = next.appendPointFrom(*tile->objects, static_cast<std::size_t>(row));
307 if (!copied.ok()) return Result<TerrainMultiTileReport>::failure(copied.status());
308 }
309 }
310 } else next = *tile->objects;
311
313 const double startX = mapping.localX * tile->width / tile->objectResolutionX + s.startOffsetX;
314 const double startZ = mapping.localY * tile->depth / tile->objectResolutionY + s.startOffsetZ;
315 const double stopX = (mapping.localX + mapping.width) * tile->width / tile->objectResolutionX + increment;
316 const double stopZ = (mapping.localY + mapping.height) * tile->depth / tile->objectResolutionY + increment;
317 const double jitter = double(s.spacing) * s.jitterPercent;
318 for (double x = startX; x <= stopX; x += increment) for (double z = startZ; z <= stopZ; z += increment, ++globalCandidate) {
319 if (random.next() < s.failureRate) continue;
320 const double localX = x + random.next(float(-jitter), float(jitter)) * 0.5;
321 const double localZ = z + random.next(float(-jitter), float(jitter)) * 0.5;
322 const int lx = nearestEven(localX * tile->objectResolutionX / tile->width);
323 const int ly = nearestEven(localZ * tile->objectResolutionY / tile->depth);
324 if (lx < mapping.localX || lx >= mapping.localX + mapping.width ||
325 ly < mapping.localY || ly >= mapping.localY + mapping.height) continue;
326 const float strength = fitness.height(mapping.operationX + lx - mapping.localX,
327 mapping.operationY + ly - mapping.localY);
328 if (random.next(s.minimumFitness, 1.0F) > strength) continue;
329 const double centerX = tile->originX + localX, centerZ = tile->originZ + localZ;
330 if (s.boundsCollisionCheck && std::any_of(acceptedCenters.begin(), acceptedCenters.end(), [&](const auto& p) {
331 return std::hypot(centerX - p.first, centerZ - p.second) < double(s.boundsRadius) * 2;
332 })) continue;
333 double sum = 0; int checks = 0;
334 const int rx = std::max(0, int(std::round(s.boundsRadius * tile->objectResolutionX / tile->width)));
335 const int ry = std::max(0, int(std::round(s.boundsRadius * tile->objectResolutionY / tile->depth)));
336 const int stepX = std::max(1, int(std::ceil(2 * rx * (1 - s.boundsCheckQuality / 100.0))));
337 const int stepY = std::max(1, int(std::ceil(2 * ry * (1 - s.boundsCheckQuality / 100.0))));
338 for (int px = lx - rx; px <= lx + rx; px += stepX) for (int py = ly - ry; py <= ly + ry; py += stepY) {
339 if (px >= mapping.localX && px < mapping.localX + mapping.width && py >= mapping.localY && py < mapping.localY + mapping.height)
340 sum += fitness.height(mapping.operationX + px - mapping.localX, mapping.operationY + py - mapping.localY);
341 ++checks;
342 }
343 if (sum / std::max(1, checks) < s.minimumFitness) continue;
344 const float direction = random.next(s.minimumDirection, s.maximumDirection);
345 acceptedCenters.emplace_back(centerX, centerZ);
346 for (std::size_t resource = 0; resource < s.instances.size(); ++resource) {
347 const auto& instance = s.instances[resource];
348 const int count = random.next(instance.minimumInstances, instance.maximumInstances);
349 for (int ordinal = 0; ordinal < count; ++ordinal) {
350 if (random.next() < instance.failureRate) continue;
351 const double radians = direction * 3.14159265358979323846 / 180.0;
352 const double ox = random.next(instance.minimumOffsetX, instance.maximumOffsetX) * s.prototypeScale;
353 const double oz = random.next(instance.minimumOffsetZ, instance.maximumOffsetZ) * s.prototypeScale;
354 const double ix = centerX + std::cos(radians) * ox - std::sin(radians) * oz;
355 const double iz = centerZ + std::sin(radians) * ox + std::cos(radians) * oz;
356 const int ilx = nearestEven((ix - tile->originX) * tile->objectResolutionX / tile->width);
357 const int ily = nearestEven((iz - tile->originZ) * tile->objectResolutionY / tile->depth);
358 if (ilx < mapping.localX || ilx >= mapping.localX + mapping.width ||
359 ily < mapping.localY || ily >= mapping.localY + mapping.height) continue;
360 const float instanceStrength = fitness.height(mapping.operationX + ilx - mapping.localX,
361 mapping.operationY + ily - mapping.localY);
362 if (instanceStrength < s.minimumInstanceFitness) continue;
363 if (emitted >= s.maxPoints)
365 DiagnosticCode::InvalidArgument, "terrain.object.multitile: point budget exceeded"));
366 const double u = (ix - tile->originX) / tile->width, v = (iz - tile->originZ) / tile->depth;
367 const Normal normal = normalAt(*tile->heights, u, v, tile->width, tile->depth, s.heightScale);
368 double iy = sample(*tile->heights, u, v) * s.heightScale;
369 if (instance.yOffsetMode == TerrainObjectYOffsetMode::SeaLevel) iy = s.seaLevel;
370 if (instance.yOffsetMode == TerrainObjectYOffsetMode::Custom) iy = instance.customOffset;
371 double sx = instance.commonScale ? instance.minimumScale : instance.minimumScaleX;
372 double sy = instance.commonScale ? instance.minimumScale : instance.minimumScaleY;
373 double sz = instance.commonScale ? instance.minimumScale : instance.minimumScaleZ;
374 if (instance.scaleMode == TerrainObjectScaleMode::Random) {
375 if (instance.commonScale) sx = sy = sz = random.next(instance.minimumScale, instance.maximumScale);
376 else { sx = random.next(instance.minimumScaleX, instance.maximumScaleX); sy = random.next(instance.minimumScaleY, instance.maximumScaleY); sz = random.next(instance.minimumScaleZ, instance.maximumScaleZ); }
377 } else if (instance.scaleMode == TerrainObjectScaleMode::Fitness || instance.scaleMode == TerrainObjectScaleMode::FitnessRandomized) {
378 if (instance.commonScale) sx = sy = sz = std::lerp(instance.minimumScale, instance.maximumScale, instanceStrength);
379 else { sx = std::lerp(instance.minimumScaleX, instance.maximumScaleX, instanceStrength); sy = std::lerp(instance.minimumScaleY, instance.maximumScaleY, instanceStrength); sz = std::lerp(instance.minimumScaleZ, instance.maximumScaleZ, instanceStrength); }
380 }
381 if (instance.scaleMode == TerrainObjectScaleMode::FitnessRandomized) {
382 if (instance.commonScale) { const double factor = random.next(1 - instance.scaleRandomPercentage, 1 + instance.scaleRandomPercentage); sx *= factor; sy *= factor; sz *= factor; }
383 else { sx *= random.next(1 - instance.scaleRandomPercentageX, 1 + instance.scaleRandomPercentageX); sy *= random.next(1 - instance.scaleRandomPercentageY, 1 + instance.scaleRandomPercentageY); sz *= random.next(1 - instance.scaleRandomPercentageZ, 1 + instance.scaleRandomPercentageZ); }
384 }
385 sx *= s.prototypeScale; sy *= s.prototypeScale; sz *= s.prototypeScale;
386 const double yOffset = random.next(instance.minimumOffsetY, instance.maximumOffsetY) * sy;
387 double fx = ix, fy = iy, fz = iz;
388 if (instance.yOffsetAlongSlope) { fx += normal.x * yOffset; fy += normal.y * yOffset; fz += normal.z * yOffset; } else fy += yOffset;
389 double pitch = random.next(instance.minimumRotationX, instance.maximumRotationX);
390 double yaw = random.next(instance.minimumRotationY + direction, instance.maximumRotationY + direction);
391 double roll = random.next(instance.minimumRotationZ, instance.maximumRotationZ);
392 if (instance.alignForwardToSlope) yaw += std::atan2(normal.x, normal.z) * 180.0 / 3.14159265358979323846;
393 if (instance.rotateToSlope) { pitch += std::atan2(-normal.z, normal.y) * 180.0 / 3.14159265358979323846; roll += std::atan2(normal.x, normal.y) * 180.0 / 3.14159265358979323846; }
394 const double radius = s.boundsRadius * std::max(sx, sz);
395 if (!isRepresentable(fx) || !isRepresentable(fy) || !isRepresentable(fz) ||
396 !isRepresentable(sx) || sx <= 0 || !isRepresentable(sy) || sy <= 0 ||
397 !isRepresentable(sz) || sz <= 0 || !isRepresentable(radius) || radius <= 0)
399 DiagnosticCode::InvalidArgument, "terrain.object.multitile: generated transform is not representable"));
401 point.id = mix(mix(mix(mix(mix(s.namespaceId) ^ stableNameHash(tile->name)) ^ (globalCandidate + 1)) ^
402 (resource + 1)) ^ (std::uint64_t(ordinal) + 1));
404 DiagnosticCode::InvalidArgument, "terrain.object.multitile: generated identity is reserved"));
405 point.x = float(fx); point.y = float(fy); point.z = float(fz);
406 point.normalX = float(normal.x); point.normalY = float(normal.y); point.normalZ = float(normal.z);
407 point.pitch = float(pitch); point.yaw = float(yaw); point.roll = float(roll);
408 point.scaleX = float(sx); point.scaleY = float(sy); point.scaleZ = float(sz); point.density = instanceStrength;
409 point.boundsMinX = point.boundsMinZ = -float(radius); point.boundsMaxX = point.boundsMaxZ = float(radius); point.boundsMaxY = float(sy);
410 point.seed = static_cast<std::uint32_t>(mix(point.id ^ static_cast<std::uint32_t>(s.seed)));
411 const int row = next.appendPoint(point);
412 auto a = next.trySetStringAttribute(row, "asset", instance.asset); if (!a.ok()) return Result<TerrainMultiTileReport>::failure(a.status());
413 auto p = next.trySetStringAttribute(row, "objectPrototype", s.prototype); if (!p.ok()) return Result<TerrainMultiTileReport>::failure(p.status());
414 auto source = next.trySetStringAttribute(row, "spawnNamespace", std::to_string(s.namespaceId)); if (!source.ok()) return Result<TerrainMultiTileReport>::failure(source.status());
415 auto c = next.trySetIntAttribute(row, "objectCandidate", static_cast<std::int64_t>(mix(stableNameHash(tile->name)) ^ mix(globalCandidate + 1))); if (!c.ok()) return Result<TerrainMultiTileReport>::failure(c.status());
416 auto r = next.trySetIntAttribute(row, "objectResource", static_cast<std::int64_t>(resource)); if (!r.ok()) return Result<TerrainMultiTileReport>::failure(r.status());
417 ++emitted; ++changed;
418 }
419 }
420 }
421 }
422 if (report.changedSamples > std::numeric_limits<int>::max() - changed)
424 DiagnosticCode::InvalidArgument, "terrain.object.multitile: changed point count exceeds range"));
425 report.changedSamples += changed;
426 candidates.push_back({tile->objects, std::move(next)});
427 }
428 for (auto& candidate : candidates) *candidate.target = std::move(candidate.value);
429 return Result<TerrainMultiTileReport>::success(std::move(report));
430}
431
433 struct Tile { std::string name; PointSet objects; Heightmap heights; double originX, originZ, width, depth; int rx, ry; bool worldMap; };
434 struct Snapshot { std::vector<PointSet> objects; TerrainMultiTileReport report; };
435 std::vector<Tile> tiles; TerrainMultiTileReport last; std::vector<Snapshot> history; int cursor = 0;
436 Snapshot snapshot() const { Snapshot result; for (const auto& tile : tiles) result.objects.push_back(tile.objects); result.report = last; return result; }
437 void restore(const Snapshot& value) { for (std::size_t i = 0; i < tiles.size(); ++i) tiles[i].objects = value.objects[i]; last = value.report; }
438};
443Result<int> TerrainMultiObjectWorkspace::addTile(const std::string& name, const PointSet& objects, const Heightmap& heights,
444 double originX, double originZ, double width, double depth,
445 int rx, int ry, bool worldMap) {
446 if (!impl_ || name.empty() || !raster_detail::validRaster(heights) || !std::isfinite(originX) ||
447 !std::isfinite(originZ) || !std::isfinite(width) || width <= 0 || !std::isfinite(depth) || depth <= 0 ||
448 rx <= 0 || ry <= 0)
449 return invalid("terrain.object.workspace: initialized named tile required");
450 if (impl_->history.size() > 1 || std::any_of(impl_->tiles.begin(), impl_->tiles.end(), [&](const auto& t) { return t.name == name; }))
451 return invalid("terrain.object.workspace: unique tile required before applying");
452 auto candidate = std::make_unique<Impl>(*impl_);
453 candidate->tiles.push_back({name, objects, heights, originX, originZ, width, depth, rx, ry, worldMap});
454 candidate->history = {candidate->snapshot()}; candidate->cursor = 0; impl_.swap(candidate);
455 return Result<int>::success(int(impl_->tiles.size()));
456}
458 const TerrainStampSettings& operationSettings,
459 TerrainObjectOperationMode mode, bool worldMapOperation) {
460 if (!impl_ || impl_->tiles.empty()) return invalid("terrain.object.workspace: populated workspace required");
461 auto candidate = std::make_unique<Impl>(*impl_); std::vector<TerrainObjectTile> tiles;
462 for (auto& tile : candidate->tiles) tiles.push_back({tile.name, &tile.objects, &tile.heights, tile.originX, tile.originZ, tile.width, tile.depth, tile.rx, tile.ry, tile.worldMap});
463 auto result = applyTerrainObjectsMultiTile(tiles, fitness, settings, operationSettings, mode, worldMapOperation);
464 if (!result.ok()) return Result<int>::failure(result.status());
465 candidate->last = std::move(result.value()); candidate->history.resize(std::size_t(candidate->cursor + 1));
466 candidate->history.push_back(candidate->snapshot()); ++candidate->cursor; const int changed = candidate->last.changedSamples;
467 impl_.swap(candidate); return Result<int>::success(changed);
468}
469Result<int> TerrainMultiObjectWorkspace::copyTile(const std::string& name, PointSet& output) const {
470 if (!impl_) return invalid("terrain.object.workspace: moved-from workspace");
471 const auto found = std::find_if(impl_->tiles.begin(), impl_->tiles.end(), [&](const auto& tile) { return tile.name == name; });
472 if (found == impl_->tiles.end()) return invalid("terrain.object.workspace: tile not found");
473 output = found->objects; return Result<int>::success(output.getCount());
474}
475Result<int> TerrainMultiObjectWorkspace::undo() { if (!impl_ || impl_->cursor <= 0) return invalid("terrain.object.workspace: no undo snapshot"); --impl_->cursor; impl_->restore(impl_->history[std::size_t(impl_->cursor)]); return Result<int>::success(impl_->cursor); }
476Result<int> TerrainMultiObjectWorkspace::redo() { if (!impl_ || impl_->cursor + 1 >= int(impl_->history.size())) return invalid("terrain.object.workspace: no redo snapshot"); ++impl_->cursor; impl_->restore(impl_->history[std::size_t(impl_->cursor)]); return Result<int>::success(impl_->cursor); }
477int TerrainMultiObjectWorkspace::getTileCount() const noexcept { return impl_ ? int(impl_->tiles.size()) : 0; }
478int TerrainMultiObjectWorkspace::getLastChangedSamples() const noexcept { return impl_ ? impl_->last.changedSamples : 0; }
479int TerrainMultiObjectWorkspace::getLastAffectedTiles() const noexcept { return impl_ ? impl_->last.affectedTiles : 0; }
480int TerrainMultiObjectWorkspace::getOperationCount() const noexcept { return impl_ ? std::max(0, int(impl_->history.size()) - 1) : 0; }
481int TerrainMultiObjectWorkspace::getAppliedCount() const noexcept { return impl_ ? impl_->cursor : 0; }
482} // 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
float cx
Definition CardTypes.cpp:33
float length
Definition CaveMesh.cpp:94
float py
float pz
glm::vec4 p[6]
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
float maximum[3]
float minimum[3]
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
double r
float v
std::int32_t c
std::vector< Colorf > px
std::uint32_t width
std::string name
MeleePoint3 a
Definition MeleeHit.cpp:40
Texture * normal
std::vector< float > scales
Definition OnnxLstm.cpp:27
float radius
std::uint32_t seed
Definition PointSet.cpp:807
float t
RoadLaneDirection direction
bool found
int removed
std::string resource
float dz
float dx
std::uint32_t count
Battle::Random random
TerrainThermalSettings settings
float offsetX
const UnitySourceAsset & source
std::uint32_t depth
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
Owning transactional multi-terrain object workspace with undo and redo snapshots.
TerrainMultiObjectWorkspace()
Terrain multi object workspace.
~TerrainMultiObjectWorkspace()
Terrain multi object workspace.
Result< int > apply(const Heightmap &fitness, const TerrainObjectPlacementSettings &settings, const TerrainStampSettings &operationSettings, TerrainObjectOperationMode mode, bool worldMapOperation=false)
Apply one operation as a single atomic history entry.
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 > removeTerrainObjectPoints(PointSet &output, const PointSet &input, const Heightmap &fitness, const TerrainObjectPlacementSettings &s, float removalStrength)
Remove matching prototype points wherever fitness is strictly above removalStrength.
Result< TerrainMultiTileReport > applyTerrainObjectsMultiTile(const std::vector< TerrainObjectTile > &tiles, const Heightmap &fitness, const TerrainObjectPlacementSettings &s, const TerrainStampSettings &operationSettings, TerrainObjectOperationMode mode, bool worldMapOperation, const std::vector< std::string > &validTerrainNames)
Apply one Pcg-style game-object rule over mapped terrain tiles atomically.
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.
TerrainObjectScaleMode
Pcg-compatible scale policy for one terrain object instance resource.
Result< int > exportTerrainObjectPoints(PointSet &output, const Heightmap &fitness, const Heightmap &heights, const TerrainObjectPlacementSettings &s)
Export Pcg-style terrain game-object resources into an attributed PointSet atomically.
TerrainObjectYOffsetMode
Height reference used by one terrain object instance resource.
TerrainObjectOperationMode
Mutation performed by a multi-terrain game-object operation.
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.
One resource entry emitted around each accepted Pcg game-object prototype location.
Deterministic Pcg SetTerrainGameObjects scan, collision and resource configuration.
std::vector< TerrainObjectInstanceSettings > instances
Result< int > addInstance(const TerrainObjectInstanceSettings &instance)
Append one validated copied resource entry and return the new resource count.
Value configuration for a rectangular stamp in world X/Z coordinates.