载入中...
搜索中...
未找到
TerrainDetailLayer.cpp
浏览该文件的文档.
2#include <algorithm>
3#include <climits>
4#include <cmath>
5#include <limits>
6#include <map>
7#include <unordered_set>
8#include <vector>
12
13namespace eve::procgen {
14namespace {
15Result<int> invalid() {
18 "terrain.detail: initialized matching finite input and valid settings required"));
19}
20class DetailThinningStream {
21 uint64_t a_, b_;
22
23public:
24 explicit DetailThinningStream(int32_t seed) {
25 const auto bits = static_cast<uint32_t>(seed == 0 ? 1 : seed);
26 a_ = uint64_t(181353) * bits;
27 b_ = uint64_t(7) * bits;
28 }
29 float next() {
30 auto x = a_, y = b_;
31 a_ = y;
32 x ^= x << 23;
33 x ^= x >> 17;
34 x ^= y ^ (y >> 26);
35 b_ = x;
36 return static_cast<float>(x + y) / static_cast<float>(UINT64_MAX);
37 }
38};
39float inverseLerp(float a, float b, float value) {
40 return a == b ? 0.0F : std::clamp((value - a) / (b - a), 0.0F, 1.0F);
41}
42int nearestEven(float value) {
43 const double lower = std::floor(double(value)), fraction = double(value) - lower;
44 return int(lower + (fraction > 0.5 || (fraction == 0.5 && std::fmod(lower, 2.0) != 0)));
45}
46bool validSettings(const TerrainDetailSettings& settings) {
47 return std::isfinite(settings.minimumFitness) && settings.minimumFitness >= 0 && settings.minimumFitness <= 1 &&
48 std::isfinite(settings.fadeStart) && settings.fadeStart >= 0 && settings.fadeStart <= 1 &&
49 std::isfinite(settings.density) && settings.density >= 0 && double(settings.density) < double(INT_MAX) &&
52}
53} // namespace
55 int width = 0, height = 0;
56 std::vector<int> counts;
57 std::map<std::uint64_t, std::vector<int>> resources;
59 std::vector<int> totals(counts.size(), 0);
60 for (const auto& [namespaceId, resourceCounts] : resources) {
61 (void)namespaceId;
62 if (resourceCounts.size() != totals.size()) return invalid();
63 for (std::size_t i = 0; i < totals.size(); ++i) {
64 if (resourceCounts[i] > INT_MAX - totals[i]) return invalid();
65 totals[i] += resourceCounts[i];
66 }
67 }
68 counts.swap(totals);
69 return Result<int>::success(static_cast<int>(counts.size()));
70 }
71};
75TerrainDetailLayer& TerrainDetailLayer::operator=(TerrainDetailLayer&&) noexcept = default;
77 : impl_(other.impl_ ? std::make_unique<Impl>(*other.impl_) : nullptr) {}
79 if (this != &other) impl_ = other.impl_ ? std::make_unique<Impl>(*other.impl_) : nullptr;
80 return *this;
81}
82int TerrainDetailLayer::getWidth() const noexcept { return impl_ ? impl_->width : 0; }
83int TerrainDetailLayer::getHeight() const noexcept { return impl_ ? impl_->height : 0; }
85 if (width <= 0 || height <= 0 || width > INT_MAX / height || count < 0) return invalid();
86 auto candidate = std::make_unique<Impl>();
87 candidate->width = width;
88 candidate->height = height;
89 candidate->counts.assign(size_t(width) * height, count);
90 candidate->resources.emplace(0, candidate->counts);
91 impl_.swap(candidate);
93}
95 if (!impl_ || x < 0 || z < 0 || x >= impl_->width || z >= impl_->height) return invalid();
96 return Result<int>::success(impl_->counts[size_t(z) * impl_->width + x]);
97}
98Result<int> TerrainDetailLayer::sampleResource(std::uint64_t namespaceId, int x, int z) const {
99 if (!impl_ || x < 0 || z < 0 || x >= impl_->width || z >= impl_->height) return invalid();
100 const auto found = impl_->resources.find(namespaceId);
101 if (found == impl_->resources.end()) return Result<int>::success(0);
102 return Result<int>::success(found->second[size_t(z) * impl_->width + x]);
103}
105 if (!impl_) return invalid();
106 auto candidate = std::make_unique<Impl>(*impl_);
107 const auto found = candidate->resources.find(namespaceId);
108 if (found == candidate->resources.end()) return Result<int>::success(0);
109 int changed = 0;
110 for (int count : found->second) changed += count != 0;
111 candidate->resources.erase(found);
112 auto rebuilt = candidate->rebuildTotals();
113 if (!rebuilt.ok()) return rebuilt;
114 impl_.swap(candidate);
115 return Result<int>::success(changed);
116}
118 if (!impl_ || fitness.getWidth() != impl_->width || fitness.getHeight() != impl_->height ||
119 fitness.data().size() != impl_->counts.size() || !validSettings(settings))
120 return invalid();
121 for (float value : fitness.data())
122 if (!std::isfinite(value)) return invalid();
123 auto next = std::make_unique<Impl>(*impl_);
124 auto [resource, inserted] = next->resources.try_emplace(settings.namespaceId, impl_->counts.size(), 0);
125 (void)inserted;
126 auto& target = resource->second;
127 if (settings.mode == TerrainDetailMode::Replace) std::fill(target.begin(), target.end(), 0);
128 DetailThinningStream random(seed);
129 const float sign = settings.mode == TerrainDetailMode::Remove ? -1.0F : 1.0F;
130 for (int x = 0; x < impl_->width; ++x)
131 for (int z = 0; z < impl_->height; ++z) {
132 const size_t index = size_t(z) * impl_->width + x;
133 const float value = fitness.data()[index];
134 if (value <= settings.minimumFitness) continue;
135 if (value < settings.fadeStart &&
136 random.next() > inverseLerp(settings.minimumFitness, settings.fadeStart, value))
137 continue;
138 const float amount = inverseLerp(settings.minimumFitness, 1.0F, value) * settings.density;
139 target[index] = nearestEven(std::clamp(float(target[index]) + sign * amount, 0.0F, settings.density));
140 }
141 auto rebuilt = next->rebuildTotals();
142 if (!rebuilt.ok()) return rebuilt;
143 int changed = 0;
144 for (size_t i = 0; i < next->counts.size(); ++i) changed += next->counts[i] != impl_->counts[i];
145 impl_.swap(next);
146 return Result<int>::success(changed);
147}
148
150 const std::vector<TerrainDetailTile>& tiles, const Heightmap& operationFitness,
151 const TerrainDetailSettings& settings, const TerrainStampSettings& operationSettings, int32_t seed,
152 bool worldMapOperation, const std::vector<std::string>& validTerrainNames) {
153 if (tiles.empty() || !validSettings(settings))
155 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.detail.multitile: valid tiles and settings required"));
156 std::vector<TerrainOperationTile> operationTiles;
157 operationTiles.reserve(tiles.size());
158 std::unordered_set<TerrainDetailLayer*> owners;
159 for (const auto& tile : tiles) {
160 if (!tile.layer || !tile.layer->impl_ || !owners.insert(tile.layer).second)
162 DiagnosticCode::InvalidArgument, "terrain.detail.multitile: distinct initialized layers required"));
163 operationTiles.push_back({tile.name, tile.originX, tile.originZ, tile.width, tile.depth,
164 tile.layer->impl_->width, tile.layer->impl_->height, tile.worldMap});
165 }
166 auto mapped = mapTerrainOperationMultiTile(operationTiles, operationSettings,
167 TerrainOperationDomain::TerrainDetail, worldMapOperation,
168 validTerrainNames);
169 if (!mapped.ok()) return mapped;
170 auto report = std::move(mapped.value());
171 if (operationFitness.getWidth() != report.operationWidth ||
172 operationFitness.getHeight() != report.operationHeight ||
173 operationFitness.data().size() != size_t(report.operationWidth) * report.operationHeight)
175 DiagnosticCode::InvalidArgument, "terrain.detail.multitile: operation fitness dimensions must match window"));
176 for (float value : operationFitness.data())
177 if (!std::isfinite(value))
179 DiagnosticCode::InvalidArgument, "terrain.detail.multitile: finite operation fitness required"));
180
181 struct Candidate {
183 std::unique_ptr<TerrainDetailLayer::Impl> value;
184 };
185 std::vector<Candidate> candidates;
186 candidates.reserve(report.mappings.size());
187 DetailThinningStream random(seed);
188 const float sign = settings.mode == TerrainDetailMode::Remove ? -1.0F : 1.0F;
189 for (const auto& mapping : report.mappings) {
190 const auto tile = std::find_if(tiles.begin(), tiles.end(), [&](const auto& item) {
191 return item.name == mapping.terrainName;
192 });
193 if (tile == tiles.end())
195 DiagnosticCode::InvalidArgument, "terrain.detail.multitile: internal mapping has no tile"));
196 auto next = std::make_unique<TerrainDetailLayer::Impl>(*tile->layer->impl_);
197 auto [resource, inserted] = next->resources.try_emplace(settings.namespaceId, next->counts.size(), 0);
198 (void)inserted;
199 auto& targetCounts = resource->second;
200 if (settings.mode == TerrainDetailMode::Replace) std::fill(targetCounts.begin(), targetCounts.end(), 0);
201 const auto& before = tile->layer->impl_->counts;
202 for (int x = 0; x < mapping.width; ++x)
203 for (int z = 0; z < mapping.height; ++z) {
204 const int localX = mapping.localX + x, localZ = mapping.localY + z;
205 const int operationX = mapping.operationX + x, operationZ = mapping.operationY + z;
206 const size_t localIndex = size_t(localZ) * next->width + localX;
207 const float fitness = operationFitness.data()[size_t(operationZ) * report.operationWidth + operationX];
208 if (fitness <= settings.minimumFitness) continue;
209 if (fitness < settings.fadeStart &&
210 random.next() > inverseLerp(settings.minimumFitness, settings.fadeStart, fitness))
211 continue;
212 const float amount = inverseLerp(settings.minimumFitness, 1.0F, fitness) * settings.density;
213 targetCounts[localIndex] = nearestEven(
214 std::clamp(float(targetCounts[localIndex]) + sign * amount, 0.0F, settings.density));
215 }
216 auto rebuilt = next->rebuildTotals();
217 if (!rebuilt.ok()) return Result<TerrainMultiTileReport>::failure(rebuilt.status());
218 for (size_t i = 0; i < before.size(); ++i) {
219 if (before[i] == next->counts[i]) continue;
220 if (report.changedSamples == std::numeric_limits<int>::max())
222 DiagnosticCode::InvalidArgument, "terrain.detail.multitile: changed cell count exceeds integer range"));
223 ++report.changedSamples;
224 }
225 candidates.push_back({tile->layer, std::move(next)});
226 }
227 for (auto& candidate : candidates) candidate.target->impl_.swap(candidate.value);
228 return Result<TerrainMultiTileReport>::success(std::move(report));
229}
230
232 struct Tile {
233 std::string name;
235 double originX = 0, originZ = 0, width = 1, depth = 1;
236 bool worldMap = false;
237 };
238 struct Snapshot {
239 std::vector<TerrainDetailLayer> layers;
241 };
242 std::vector<Tile> tiles;
244 std::vector<Snapshot> history;
245 int cursor = 0;
246
247 Impl() = default;
248 Impl(const Impl& other) : last(other.last), history(other.history), cursor(other.cursor) {
249 tiles.reserve(other.tiles.size());
250 for (const auto& source : other.tiles) {
252 layer.impl_ = std::make_unique<TerrainDetailLayer::Impl>(*source.layer.impl_);
253 tiles.push_back({source.name, std::move(layer), source.originX, source.originZ, source.width,
254 source.depth, source.worldMap});
255 }
256 }
258 Snapshot result;
259 result.layers.reserve(tiles.size());
260 for (const auto& tile : tiles) result.layers.push_back(tile.layer);
261 result.report = last;
262 return result;
263 }
264 void restore(const Snapshot& snapshot) {
265 for (size_t i = 0; i < tiles.size(); ++i) tiles[i].layer = snapshot.layers[i];
267 }
268};
269
274
276 double originX, double originZ, double width, double depth,
277 bool worldMap) {
278 if (!impl_ || !layer.impl_ || name.empty())
280 DiagnosticCode::InvalidArgument, "terrain.detail.workspace: initialized named tile required"));
281 if (std::any_of(impl_->tiles.begin(), impl_->tiles.end(), [&](const auto& tile) { return tile.name == name; }))
283 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.detail.workspace: duplicate tile name"));
284 if (impl_->history.size() > 1)
286 DiagnosticCode::InvalidArgument, "terrain.detail.workspace: topology is fixed after applying"));
287 auto candidate = std::make_unique<Impl>(*impl_);
289 owned.impl_ = std::make_unique<TerrainDetailLayer::Impl>(*layer.impl_);
290 candidate->tiles.push_back({name, std::move(owned), originX, originZ, width, depth, worldMap});
291 std::vector<TerrainOperationTile> descriptors;
292 for (const auto& tile : candidate->tiles)
293 descriptors.push_back({tile.name, tile.originX, tile.originZ, tile.width, tile.depth,
294 tile.layer.impl_->width, tile.layer.impl_->height, tile.worldMap});
296 bounds.centerX = originX + width * 0.5;
297 bounds.centerZ = originZ + depth * 0.5;
298 bounds.width = width;
299 bounds.depth = depth;
301 worldMap, {name});
302 if (!checked.ok()) return Result<int>::failure(checked.status());
303 candidate->history.clear();
304 candidate->history.push_back(candidate->snapshot());
305 candidate->cursor = 0;
306 impl_.swap(candidate);
307 return Result<int>::success(int(impl_->tiles.size()));
308}
309
310Result<int> TerrainMultiDetailWorkspace::apply(const Heightmap& operationFitness,
312 const TerrainStampSettings& operationSettings, int32_t seed,
313 bool worldMapOperation) {
314 if (!impl_)
316 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.detail.workspace: moved-from workspace"));
317 auto candidate = std::make_unique<Impl>(*impl_);
318 std::vector<TerrainDetailTile> descriptors;
319 for (auto& tile : candidate->tiles)
320 descriptors.push_back({tile.name, &tile.layer, tile.originX, tile.originZ, tile.width, tile.depth,
321 tile.worldMap});
322 auto applied = applyTerrainDetailMultiTile(descriptors, operationFitness, settings, operationSettings, seed,
323 worldMapOperation);
324 if (!applied.ok()) return Result<int>::failure(applied.status());
325 candidate->last = std::move(applied.value());
326 candidate->history.resize(size_t(candidate->cursor + 1));
327 candidate->history.push_back(candidate->snapshot());
328 ++candidate->cursor;
329 const int changed = candidate->last.changedSamples;
330 impl_.swap(candidate);
331 return Result<int>::success(changed);
332}
333
334Result<int> TerrainMultiDetailWorkspace::copyTile(const std::string& name, TerrainDetailLayer& output) const {
335 if (!impl_)
337 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.detail.workspace: moved-from workspace"));
338 const auto found = std::find_if(impl_->tiles.begin(), impl_->tiles.end(),
339 [&](const auto& tile) { return tile.name == name; });
340 if (found == impl_->tiles.end())
342 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.detail.workspace: tile not found"));
343 auto copy = std::make_unique<TerrainDetailLayer::Impl>(*found->layer.impl_);
344 output.impl_.swap(copy);
345 return Result<int>::success(found->layer.impl_->width * found->layer.impl_->height);
346}
347
348Result<int> TerrainMultiDetailWorkspace::undo() {
349 if (!impl_ || impl_->cursor <= 0)
351 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.detail.workspace: no undo snapshot"));
352 auto candidate = std::make_unique<Impl>(*impl_);
353 --candidate->cursor;
354 candidate->restore(candidate->history[size_t(candidate->cursor)]);
355 const int cursor = candidate->cursor;
356 impl_.swap(candidate);
358}
359Result<int> TerrainMultiDetailWorkspace::redo() {
360 if (!impl_ || impl_->cursor + 1 >= int(impl_->history.size()))
362 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.detail.workspace: no redo snapshot"));
363 auto candidate = std::make_unique<Impl>(*impl_);
364 ++candidate->cursor;
365 candidate->restore(candidate->history[size_t(candidate->cursor)]);
366 const int cursor = candidate->cursor;
367 impl_.swap(candidate);
369}
370int TerrainMultiDetailWorkspace::getTileCount() const noexcept { return impl_ ? int(impl_->tiles.size()) : 0; }
371int TerrainMultiDetailWorkspace::getLastChangedSamples() const noexcept {
372 return impl_ ? impl_->last.changedSamples : 0;
373}
374int TerrainMultiDetailWorkspace::getLastAffectedTiles() const noexcept {
375 return impl_ ? impl_->last.affectedTiles : 0;
376}
377int TerrainMultiDetailWorkspace::getOperationCount() const noexcept {
378 return impl_ ? std::max(0, int(impl_->history.size()) - 1) : 0;
379}
380int TerrainMultiDetailWorkspace::getAppliedCount() const noexcept { return impl_ ? impl_->cursor : 0; }
381} // 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
ActiveSource owned
std::uint32_t height
std::uint32_t width
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
TileLayer * layer
std::uint32_t seed
Definition PointSet.cpp:807
bool found
std::string resource
std::uint32_t count
std::size_t cursor
Battle::Random random
TerrainThermalSettings settings
uint32_t index
const UnitySourceAsset & source
std::uint32_t depth
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
int getWidth() const
Returns the width.
Definition Heightmap.cpp:16
Owned integer terrain-detail layer, independent of graphics and resource registries....
TerrainDetailLayer()
Construct an empty layer.
Result< int > reset(int width, int height, int count=0)
Reset positive dimensions and uniform nonnegative count atomically.
TerrainDetailLayer & operator=(TerrainDetailLayer &&) noexcept
Transfer ownership, releasing previous counts.
Result< int > sample(int x, int z) const
Read an integer value copy; empty state or invalid coordinates return InvalidArgument.
Result< int > clearResource(std::uint64_t namespaceId)
Remove one resource layer atomically and return its changed cell count.
~TerrainDetailLayer()
Release owned counts.
Result< int > apply(const Heightmap &fitness, const TerrainDetailSettings &settings, int32_t seed)
Apply a finite aligned fitness map with source thinning and nearest-even integer rounding.
int getWidth() const noexcept
Return width, zero when empty or moved-from.
int getHeight() const noexcept
Return height, zero when empty or moved-from.
Result< int > sampleResource(std::uint64_t namespaceId, int x, int z) const
Read one resource layer without exposing mutable storage; an absent namespace reads as zero.
Owning script-safe multi-terrain detail transaction workspace. Input layers are copied on insertion....
~TerrainMultiDetailWorkspace()
Terrain multi detail workspace.
TerrainMultiDetailWorkspace()
Terrain multi detail workspace.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
Result< int > invalid(std::string message)
Invalid.
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 > applyTerrainDetailMultiTile(const std::vector< TerrainDetailTile > &tiles, const Heightmap &operationFitness, const TerrainDetailSettings &settings, const TerrainStampSettings &operationSettings, int32_t seed, bool worldMapOperation, const std::vector< std::string > &validTerrainNames)
std::map< std::uint64_t, std::vector< int > > resources
Effective detail rule settings; density is rule density times global spawn density.
Completed multi-tile stamp statistics and Pcg-compatible affected-pixel mappings.
Value configuration for a rectangular stamp in world X/Z coordinates.
glm::vec4 bounds