载入中...
搜索中...
未找到
TerrainSplatmap.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <cstdint>
6#include <limits>
7#include <unordered_set>
8#include <vector>
9
14
15namespace eve::procgen {
16namespace {
17
18float fade(float value) { return value * value * value * (value * (value * 6.f - 15.f) + 10.f); }
19float seamHash(int x, int y) {
20 std::uint32_t value = std::uint32_t(x) * 0x8da6b343u ^ std::uint32_t(y) * 0xd8163841u;
21 value ^= value >> 13;
22 value *= 0xcb1ab31fu;
23 value ^= value >> 16;
24 return float(value & 0x00ffffffu) / float(0x00ffffffu);
25}
26float seamNoise(float x, float y) {
27 const int x0 = static_cast<int>(std::floor(x)), y0 = static_cast<int>(std::floor(y));
28 const float tx = fade(x - float(x0)), ty = fade(y - float(y0));
29 const float a = std::lerp(seamHash(x0, y0), seamHash(x0 + 1, y0), tx);
30 const float b = std::lerp(seamHash(x0, y0 + 1), seamHash(x0 + 1, y0 + 1), tx);
31 return std::lerp(a, b, ty);
32}
33}
35 int width = 0, height = 0, layers = 0, lastChanged = 0;
36 std::vector<float> weights;
37 std::size_t index(int layer, int x, int y) const {
38 return (static_cast<std::size_t>(layer) * height + y) * width + x;
39 }
40};
43 std::vector<Layer> layers;
44};
45GtsHeightBlendSet::GtsHeightBlendSet() : impl_(std::make_unique<Impl>()) {}
48GtsHeightBlendSet& GtsHeightBlendSet::operator=(GtsHeightBlendSet&&) noexcept = default;
49Result<int> GtsHeightBlendSet::addLayer(const Heightmap& height, float contrast, float brightness, float increase) {
50 using namespace raster_detail;
51 if (!validRaster(height) || !std::isfinite(contrast) || contrast < 0 || !std::isfinite(brightness) ||
52 !std::isfinite(increase)) return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.gtsHeightBlend: finite layer transform required"));
53 if (!impl_) impl_ = std::make_unique<Impl>();
54 if (impl_->layers.size() >= 8) return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.gtsHeightBlend: at most eight layers supported"));
55 if (!impl_->layers.empty() && (height.getWidth() != impl_->layers[0].height.getWidth() ||
56 height.getHeight() != impl_->layers[0].height.getHeight()))
57 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.gtsHeightBlend: all height rasters must match"));
58 impl_->layers.push_back({height, contrast, brightness, increase});
59 return Result<int>::success(static_cast<int>(impl_->layers.size()));
60}
61int GtsHeightBlendSet::getLayerCount() const noexcept { return impl_ ? static_cast<int>(impl_->layers.size()) : 0; }
62TerrainSplatmap::TerrainSplatmap() : impl_(std::make_unique<Impl>()) {}
65TerrainSplatmap& TerrainSplatmap::operator=(TerrainSplatmap&&) noexcept = default;
67 : impl_(other.impl_ ? std::make_unique<Impl>(*other.impl_) : std::make_unique<Impl>()) {}
69 if (this != &other) {
70 if (!impl_) impl_ = std::make_unique<Impl>();
71 *impl_ = other.impl_ ? *other.impl_ : Impl{};
72 }
73 return *this;
74}
75Result<int> TerrainSplatmap::initialize(int width, int height, int layers, int defaultLayer) {
76 if (width <= 0 || height <= 0 || layers <= 0 || defaultLayer < 0 || defaultLayer >= layers)
77 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splatmap: positive topology and valid default layer required"));
78 const auto texels = std::uint64_t(width) * std::uint64_t(height);
79 if (texels > std::numeric_limits<std::size_t>::max() / std::uint64_t(layers) ||
80 texels * std::uint64_t(layers) > std::numeric_limits<int>::max())
81 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splatmap: topology exceeds supported size"));
82 if (!impl_) impl_ = std::make_unique<Impl>();
83 Impl next;
84 next.width = width; next.height = height; next.layers = layers;
85 next.weights.assign(static_cast<std::size_t>(texels) * layers, 0);
86 for (int y = 0; y < height; ++y)
87 for (int x = 0; x < width; ++x) next.weights[next.index(defaultLayer, x, y)] = 1;
88 *impl_ = std::move(next);
89 return Result<int>::success(static_cast<int>(texels));
90}
92 if (!impl_ || impl_->layers <= 0) return Result<float>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splatmap: initialize before sampling"));
93 if (layer < 0 || layer >= impl_->layers || x < 0 || x >= impl_->width || y < 0 || y >= impl_->height)
94 return Result<float>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splatmap: sample index out of range"));
95 return Result<float>::success(impl_->weights[impl_->index(layer, x, y)]);
96}
97
99 if (!impl_ || impl_->width <= 0 || layer < 0 || layer >= impl_->layers || output.getWidth() != impl_->width ||
100 output.getHeight() != impl_->height)
101 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splat.copyLayer: initialized source, valid layer and matching output required"));
102 std::vector<float> candidate(size_t(impl_->width) * impl_->height);
103 for (int y = 0; y < impl_->height; ++y)
104 for (int x = 0; x < impl_->width; ++x)
105 candidate[size_t(y) * impl_->width + x] = impl_->weights[impl_->index(layer, x, y)];
106 int changed = 0;
107 for (size_t i = 0; i < candidate.size(); ++i) changed += candidate[i] != output.data()[i];
108 output.data().swap(candidate);
109 return Result<int>::success(changed);
110}
111int TerrainSplatmap::getLastChangedSamples() const noexcept { return impl_ ? impl_->lastChanged : 0; }
112int TerrainSplatmap::getWidth() const noexcept { return impl_ ? impl_->width : 0; }
113int TerrainSplatmap::getHeight() const noexcept { return impl_ ? impl_->height : 0; }
114int TerrainSplatmap::getLayerCount() const noexcept { return impl_ ? impl_->layers : 0; }
115
118 if (&terrainA == &terrainB || !terrainA.impl_ || !terrainB.impl_ || terrainA.impl_->layers <= 0 ||
119 terrainB.impl_->layers <= 0 || terrainA.impl_->width <= 0 || terrainA.impl_->height <= 0 ||
120 terrainB.impl_->width <= 0 || terrainB.impl_->height <= 0 ||
121 terrainA.impl_->width != terrainA.impl_->height || terrainB.impl_->width != terrainB.impl_->height ||
122 !std::isfinite(settings.terrainAOriginX) || !std::isfinite(settings.terrainAOriginZ) ||
123 !std::isfinite(settings.terrainAWidth) || !std::isfinite(settings.terrainADepth) ||
124 !std::isfinite(settings.terrainBOriginX) || !std::isfinite(settings.terrainBOriginZ) ||
125 !std::isfinite(settings.terrainBWidth) || !std::isfinite(settings.terrainBDepth) ||
126 !std::isfinite(settings.blendStrength) || !std::isfinite(settings.adjacencyTolerance) ||
127 settings.terrainAWidth <= 0 || settings.terrainADepth <= 0 || settings.terrainBWidth <= 0 ||
128 settings.terrainBDepth <= 0 || settings.blendStrength < 0 || settings.blendStrength > 1 ||
129 settings.blendWidth <= 0 || settings.adjacencyTolerance <= 0)
130 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.textureAlign: distinct initialized square maps and valid finite settings required"));
131
132 enum class Edge { None, Left, Right, Top, Bottom };
133 Edge edgeA = Edge::None, edgeB = Edge::None;
134 const double aMinX = settings.terrainAOriginX, aMaxX = aMinX + settings.terrainAWidth;
135 const double aMinZ = settings.terrainAOriginZ, aMaxZ = aMinZ + settings.terrainADepth;
136 const double bMinX = settings.terrainBOriginX, bMaxX = bMinX + settings.terrainBWidth;
137 const double bMinZ = settings.terrainBOriginZ, bMaxZ = bMinZ + settings.terrainBDepth;
138 const auto near = [&](double a, double b) { return std::abs(a - b) < settings.adjacencyTolerance; };
139 const bool xOverlaps = (bMaxX > aMinX && bMinX < aMaxX) || near(bMaxX, aMinX) ||
140 near(bMinX, aMaxX) || near(aMinX, bMinX) || near(aMaxX, bMaxX);
141 if (xOverlaps && near(aMaxZ, bMinZ)) edgeA = Edge::Right, edgeB = Edge::Left;
142 else if (xOverlaps && near(aMinZ, bMaxZ)) edgeA = Edge::Left, edgeB = Edge::Right;
143 else {
144 const bool zOverlaps = (bMaxZ > aMinZ && bMinZ < aMaxZ) || near(bMaxZ, aMinZ) ||
145 near(bMinZ, aMaxZ) || near(aMinZ, bMinZ) || near(aMaxZ, bMaxZ);
146 if (zOverlaps && near(aMaxX, bMinX)) edgeA = Edge::Top, edgeB = Edge::Bottom;
147 else if (zOverlaps && near(aMinX, bMaxX)) edgeA = Edge::Bottom, edgeB = Edge::Top;
148 }
149 if (edgeA == Edge::None)
150 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.textureAlign: terrain bounds are not adjacent"));
151
152 auto nextA = *terrainA.impl_, nextB = *terrainB.impl_;
153 const int resolution = std::min(nextA.width, nextB.width);
154 const int layers = std::min(nextA.layers, nextB.layers);
155 const bool horizontal = edgeA == Edge::Left || edgeA == Edge::Right;
156 int changedA = 0, changedB = 0;
157 for (int i = 0; i < settings.blendWidth; ++i) {
158 const float t = std::clamp(float(i) / float(settings.blendWidth), 0.f, 1.f);
159 const float blend = settings.blendStrength * (1.f - t * t * (3.f - 2.f * t));
160 for (int j = 0; j < resolution; ++j) {
161 const int depthA = std::min(i, resolution - 1), depthB = std::min(i, resolution - 1);
162 const int xA = horizontal ? (edgeA == Edge::Left ? depthA : resolution - 1 - depthA) : j;
163 const int yA = horizontal ? j : (edgeA == Edge::Bottom ? depthA : resolution - 1 - depthA);
164 const int xB = horizontal ? (edgeB == Edge::Left ? depthB : resolution - 1 - depthB) : j;
165 const int yB = horizontal ? j : (edgeB == Edge::Bottom ? depthB : resolution - 1 - depthB);
166 const float noise = seamNoise(horizontal ? j * 0.1f : i * 0.1f,
167 horizontal ? i * 0.1f : j * 0.1f) * 0.5f + 0.5f;
168 const float weightA = noise * blend, weightB = (1.f - noise) * blend;
169 std::vector<float> oldA(static_cast<std::size_t>(layers));
170 std::vector<float> oldB(static_cast<std::size_t>(layers));
171 for (int layer = 0; layer < layers; ++layer) {
172 oldA[static_cast<std::size_t>(layer)] = terrainA.impl_->weights[terrainA.impl_->index(layer, xA, yA)];
173 oldB[static_cast<std::size_t>(layer)] = terrainB.impl_->weights[terrainB.impl_->index(layer, xB, yB)];
174 }
175 float sumA = 0, sumB = 0;
176 for (int layer = 0; layer < layers; ++layer) {
177 const float a = oldA[static_cast<std::size_t>(layer)], b = oldB[static_cast<std::size_t>(layer)];
178 nextA.weights[nextA.index(layer, xA, yA)] = std::lerp(a, b, weightA);
179 nextB.weights[nextB.index(layer, xB, yB)] = std::lerp(b, a, weightB);
180 sumA += nextA.weights[nextA.index(layer, xA, yA)];
181 sumB += nextB.weights[nextB.index(layer, xB, yB)];
182 }
183 if (sumA > 0 && sumB > 0) for (int layer = 0; layer < layers; ++layer) {
184 nextA.weights[nextA.index(layer, xA, yA)] /= sumA;
185 nextB.weights[nextB.index(layer, xB, yB)] /= sumB;
186 }
187 bool aChanged = false, bChanged = false;
188 for (int layer = 0; layer < layers; ++layer) {
189 aChanged |= nextA.weights[nextA.index(layer, xA, yA)] != oldA[static_cast<std::size_t>(layer)];
190 bChanged |= nextB.weights[nextB.index(layer, xB, yB)] != oldB[static_cast<std::size_t>(layer)];
191 }
192 changedA += aChanged;
193 changedB += bChanged;
194 }
195 }
196 nextA.lastChanged = changedA;
197 nextB.lastChanged = changedB;
198 *terrainA.impl_ = std::move(nextA);
199 *terrainB.impl_ = std::move(nextB);
200 return Result<int>::success(changedA + changedB);
201}
202
204 using namespace raster_detail;
205 if (!target.impl_ || target.impl_->layers <= 0 || !validRaster(paint) ||
206 paint.getWidth() != target.impl_->width || paint.getHeight() != target.impl_->height ||
207 targetLayer < 0 || targetLayer >= target.impl_->layers ||
208 !std::all_of(paint.data().begin(), paint.data().end(), [](float value) {
209 return std::isfinite(value) && value >= 0 && value <= 1;
210 }) || (target.impl_->layers == 1 && std::any_of(paint.data().begin(), paint.data().end(),
211 [](float value) { return value != 1; })))
212 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.paintSplat: initialized matching topology, layer and normalized paint required"));
213 auto next = *target.impl_;
214 int changed = 0;
215 for (int y = 0; y < next.height; ++y) {
216 for (int x = 0; x < next.width; ++x) {
217 const auto targetIndex = next.index(targetLayer, x, y);
218 const float oldTarget = next.weights[targetIndex], replacement = paint.height(x, y);
219 if (oldTarget != replacement) ++changed;
220 const double oldOther = 1.0 - oldTarget;
221 if (oldOther > 1e-7) {
222 const double factor = (1.0 - replacement) / oldOther;
223 for (int layer = 0; layer < next.layers; ++layer)
224 if (layer != targetLayer) next.weights[next.index(layer, x, y)] =
225 static_cast<float>(next.weights[next.index(layer, x, y)] * factor);
226 } else {
227 const float share = next.layers > 1 ? (1 - replacement) / float(next.layers - 1) : 0;
228 for (int layer = 0; layer < next.layers; ++layer)
229 if (layer != targetLayer) next.weights[next.index(layer, x, y)] = share;
230 }
231 next.weights[targetIndex] = replacement;
232 }
233 }
234 next.lastChanged = changed;
235 *target.impl_ = std::move(next);
236 return Result<int>::success(changed);
237}
238
240 const GtsHeightBlendSet& heights, float blendFactor) {
241 if (!input.impl_ || input.impl_->layers <= 0 || !heights.impl_ ||
242 heights.impl_->layers.size() != static_cast<std::size_t>(input.impl_->layers) ||
243 !std::isfinite(blendFactor) || blendFactor < 0 || blendFactor > 1)
244 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.gtsHeightBlend: initialized splat, matching heights and normalized factor required"));
245 for (const auto& layer : heights.impl_->layers)
246 if (layer.height.getWidth() != input.impl_->width || layer.height.getHeight() != input.impl_->height)
247 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.gtsHeightBlend: topology mismatch"));
248 auto next = *input.impl_;
249 int changed = 0;
250 std::vector<float> weighted(static_cast<std::size_t>(next.layers));
251 for (int y = 0; y < next.height; ++y) for (int x = 0; x < next.width; ++x) {
252 float maxHeight = 0;
253 for (int layer = 0; layer < next.layers; ++layer) {
254 const auto& source = heights.impl_->layers[static_cast<std::size_t>(layer)];
255 const float transformed = std::pow(std::abs(source.height.height(x, y)), source.contrast) *
256 source.brightness + source.increase;
257 weighted[static_cast<std::size_t>(layer)] = transformed * input.impl_->weights[input.impl_->index(layer, x, y)];
258 maxHeight = std::max(maxHeight, weighted[static_cast<std::size_t>(layer)]);
259 }
260 float sum = 0;
261 const float transition = std::max(blendFactor, 1e-5F);
262 for (int layer = 0; layer < next.layers; ++layer) {
263 float& value = weighted[static_cast<std::size_t>(layer)];
264 value = std::max(0.0F, value + transition - maxHeight);
265 value = (value + 1e-6F) * input.impl_->weights[input.impl_->index(layer, x, y)];
266 sum += value;
267 }
268 sum = std::max(sum, 1e-6F);
269 bool texelChanged = false;
270 for (int layer = 0; layer < next.layers; ++layer) {
271 const auto index = next.index(layer, x, y);
272 const float value = weighted[static_cast<std::size_t>(layer)] / sum;
273 texelChanged |= value != next.weights[index]; next.weights[index] = value;
274 }
275 changed += texelChanged;
276 }
277 next.lastChanged = changed;
278 if (!output.impl_) output.impl_ = std::make_unique<TerrainSplatmap::Impl>();
279 *output.impl_ = std::move(next);
280 return Result<int>::success(changed);
281}
282
284 const std::vector<TerrainSplatTile>& tiles, const Heightmap& operationPaint, int targetLayer,
285 const TerrainStampSettings& operationSettings, bool worldMapOperation,
286 const std::vector<std::string>& validTerrainNames) {
287 using namespace raster_detail;
288 if (tiles.empty() || !validRaster(operationPaint) ||
289 !std::all_of(operationPaint.data().begin(), operationPaint.data().end(), [](float value) {
290 return std::isfinite(value) && value >= 0 && value <= 1;
291 }))
292 return Result<TerrainMultiTileReport>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.paintSplat.multitile: tiles and normalized paint required"));
293 std::vector<TerrainOperationTile> descriptors;
294 std::unordered_set<TerrainSplatmap*> owners;
295 descriptors.reserve(tiles.size());
296 for (const auto& tile : tiles) {
297 if (!tile.splatmap || !tile.splatmap->impl_ || tile.splatmap->impl_->layers <= 0 ||
298 !owners.insert(tile.splatmap).second || targetLayer < 0 || targetLayer >= tile.splatmap->impl_->layers)
299 return Result<TerrainMultiTileReport>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.paintSplat.multitile: distinct initialized owners and common target layer required"));
300 descriptors.push_back({tile.name, tile.originX, tile.originZ, tile.width, tile.depth,
301 tile.splatmap->impl_->width, tile.splatmap->impl_->height, tile.worldMap});
302 }
303 auto mapped = mapTerrainOperationMultiTile(descriptors, operationSettings, TerrainOperationDomain::Texture,
304 worldMapOperation, validTerrainNames);
305 if (!mapped.ok()) return mapped;
306 auto report = std::move(mapped.value());
307 if (operationPaint.getWidth() != report.operationWidth || operationPaint.getHeight() != report.operationHeight)
308 return Result<TerrainMultiTileReport>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.paintSplat.multitile: paint dimensions must match the shared operation window"));
309 struct Candidate { TerrainSplatmap* target; TerrainSplatmap::Impl value; };
310 std::vector<Candidate> candidates;
311 candidates.reserve(report.mappings.size());
312 for (const auto& mapping : report.mappings) {
313 const auto tile = std::find_if(tiles.begin(), tiles.end(),
314 [&](const auto& value) { return value.name == mapping.terrainName; });
315 if (tile == tiles.end())
317 DiagnosticCode::InvariantViolation, "terrain.paintSplat.multitile: mapped terrain was not supplied"));
318 if (tile->splatmap->impl_->layers == 1) {
319 for (int y = 0; y < mapping.height; ++y)
320 for (int x = 0; x < mapping.width; ++x)
321 if (operationPaint.height(mapping.operationX + x, mapping.operationY + y) != 1)
322 return Result<TerrainMultiTileReport>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.paintSplat.multitile: a selected single-layer splatmap must remain one"));
323 }
324 auto next = *tile->splatmap->impl_;
325 int changed = 0;
326 for (int y = 0; y < mapping.height; ++y) {
327 for (int x = 0; x < mapping.width; ++x) {
328 const int localX = mapping.localX + x, localY = mapping.localY + y;
329 const float replacement = operationPaint.height(mapping.operationX + x, mapping.operationY + y);
330 const auto index = next.index(targetLayer, localX, localY);
331 const float oldTarget = next.weights[index];
332 if (oldTarget != replacement) ++changed;
333 const double oldOther = 1.0 - oldTarget;
334 if (oldOther > 1e-7) {
335 const double factor = (1.0 - replacement) / oldOther;
336 for (int layer = 0; layer < next.layers; ++layer)
337 if (layer != targetLayer) next.weights[next.index(layer, localX, localY)] =
338 static_cast<float>(next.weights[next.index(layer, localX, localY)] * factor);
339 } else {
340 const float share = next.layers > 1 ? (1 - replacement) / float(next.layers - 1) : 0;
341 for (int layer = 0; layer < next.layers; ++layer)
342 if (layer != targetLayer) next.weights[next.index(layer, localX, localY)] = share;
343 }
344 next.weights[index] = replacement;
345 }
346 }
347 next.lastChanged = changed;
348 if (report.changedSamples > std::numeric_limits<int>::max() - changed)
349 return Result<TerrainMultiTileReport>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.paintSplat.multitile: changed texel count exceeds range"));
350 report.changedSamples += changed;
351 candidates.push_back({tile->splatmap, std::move(next)});
352 }
353 for (auto& candidate : candidates) *candidate.target->impl_ = std::move(candidate.value);
354 return Result<TerrainMultiTileReport>::success(std::move(report));
355}
356
358 struct Tile {
359 std::string name;
361 double originX = 0, originZ = 0, width = 1, depth = 1;
362 bool worldMap = false;
363 };
364 struct Snapshot {
365 std::vector<TerrainSplatmap> maps;
367 };
368 std::vector<Tile> tiles;
370 std::vector<Snapshot> history;
371 int cursor = 0;
374 value.maps.reserve(tiles.size());
375 for (const auto& tile : tiles) value.maps.push_back(tile.splatmap);
376 value.report = last;
377 return value;
378 }
379 void restore(const Snapshot& value) {
380 for (std::size_t i = 0; i < tiles.size(); ++i) tiles[i].splatmap = value.maps[i];
381 last = value.report;
382 }
383};
384
389
390Result<int> TerrainMultiSplatWorkspace::addTile(const std::string& name, const TerrainSplatmap& splatmap,
391 double originX, double originZ, double width, double depth,
392 bool worldMap) {
393 if (!impl_ || name.empty() || splatmap.getWidth() <= 0 || splatmap.getHeight() <= 0)
394 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splat.workspace: initialized named tile required"));
395 if (std::any_of(impl_->tiles.begin(), impl_->tiles.end(), [&](const auto& tile) { return tile.name == name; }))
396 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splat.workspace: duplicate tile name"));
397 if (impl_->history.size() > 1)
398 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splat.workspace: topology is fixed after painting"));
399 auto candidate = std::make_unique<Impl>(*impl_);
400 candidate->tiles.push_back({name, splatmap, originX, originZ, width, depth, worldMap});
401 std::vector<TerrainOperationTile> descriptors;
402 for (const auto& tile : candidate->tiles)
403 descriptors.push_back({tile.name, tile.originX, tile.originZ, tile.width, tile.depth,
404 tile.splatmap.getWidth(), tile.splatmap.getHeight(), tile.worldMap});
406 bounds.centerX = originX + width * 0.5;
407 bounds.centerZ = originZ + depth * 0.5;
408 bounds.width = width;
409 bounds.depth = depth;
410 auto checked = mapTerrainOperationMultiTile(descriptors, bounds, TerrainOperationDomain::Texture, worldMap, {name});
411 if (!checked.ok()) return Result<int>::failure(checked.status());
412 candidate->history.clear();
413 candidate->history.push_back(candidate->snapshot());
414 candidate->cursor = 0;
415 impl_.swap(candidate);
416 return Result<int>::success(static_cast<int>(impl_->tiles.size()));
417}
418
419Result<int> TerrainMultiSplatWorkspace::paint(const Heightmap& operationPaint, int targetLayer,
420 const TerrainStampSettings& operationSettings,
421 bool worldMapOperation) {
422 if (!impl_) return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splat.workspace: moved-from workspace"));
423 auto candidate = std::make_unique<Impl>(*impl_);
424 std::vector<TerrainSplatTile> descriptors;
425 for (auto& tile : candidate->tiles)
426 descriptors.push_back({tile.name, &tile.splatmap, tile.originX, tile.originZ, tile.width, tile.depth,
427 tile.worldMap});
428 auto painted = paintTerrainSplatLayerMultiTile(descriptors, operationPaint, targetLayer, operationSettings,
429 worldMapOperation);
430 if (!painted.ok()) return Result<int>::failure(painted.status());
431 candidate->last = std::move(painted.value());
432 candidate->history.resize(static_cast<std::size_t>(candidate->cursor + 1));
433 candidate->history.push_back(candidate->snapshot());
434 ++candidate->cursor;
435 const int changed = candidate->last.changedSamples;
436 impl_.swap(candidate);
437 return Result<int>::success(changed);
438}
439
440Result<int> TerrainMultiSplatWorkspace::copyTile(const std::string& name, TerrainSplatmap& output) const {
441 if (!impl_) return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splat.workspace: moved-from workspace"));
442 const auto found = std::find_if(impl_->tiles.begin(), impl_->tiles.end(),
443 [&](const auto& tile) { return tile.name == name; });
444 if (found == impl_->tiles.end()) return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splat.workspace: tile not found"));
445 TerrainSplatmap copy = found->splatmap;
446 output = std::move(copy);
447 return Result<int>::success(output.getWidth() * output.getHeight());
448}
449Result<int> TerrainMultiSplatWorkspace::undo() {
450 if (!impl_ || impl_->cursor <= 0) return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splat.workspace: no undo snapshot"));
451 --impl_->cursor;
452 impl_->restore(impl_->history[static_cast<std::size_t>(impl_->cursor)]);
453 return Result<int>::success(impl_->cursor);
454}
455Result<int> TerrainMultiSplatWorkspace::redo() {
456 if (!impl_ || impl_->cursor + 1 >= static_cast<int>(impl_->history.size()))
457 return Result<int>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.splat.workspace: no redo snapshot"));
458 ++impl_->cursor;
459 impl_->restore(impl_->history[static_cast<std::size_t>(impl_->cursor)]);
460 return Result<int>::success(impl_->cursor);
461}
462int TerrainMultiSplatWorkspace::getTileCount() const noexcept { return impl_ ? static_cast<int>(impl_->tiles.size()) : 0; }
463int TerrainMultiSplatWorkspace::getLastChangedSamples() const noexcept { return impl_ ? impl_->last.changedSamples : 0; }
464int TerrainMultiSplatWorkspace::getLastAffectedTiles() const noexcept { return impl_ ? impl_->last.affectedTiles : 0; }
465int TerrainMultiSplatWorkspace::getOperationCount() const noexcept {
466 return impl_ ? std::max(0, static_cast<int>(impl_->history.size()) - 1) : 0;
467}
468int TerrainMultiSplatWorkspace::getAppliedCount() const noexcept { return impl_ ? impl_->cursor : 0; }
469} // namespace eve::procgen
LogicalId target
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
std::string output
float contrast
EvpackChunkInput input
Definition Evpack.cpp:170
float blend
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
float t
bool found
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
Owning ordered GTS layer-height rasters and their alpha-channel transforms.
GtsHeightBlendSet()
Gts height blend set.
int getLayerCount() const noexcept
Return the number of owned height layers.
~GtsHeightBlendSet()
Gts height blend set.
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
Owning script-safe multi-terrain splat transaction and all-tile undo history. Input maps are copied....
~TerrainMultiSplatWorkspace()
Terrain multi splat workspace.
TerrainMultiSplatWorkspace()
Terrain multi splat workspace.
Owning normalized terrain texture-layer weights with atomic mutation. Layers share one finite width/h...
int getHeight() const noexcept
Current splat height, or zero before initialization.
Result< int > copyLayer(int layer, Heightmap &output) const
Copy one complete texture layer into a same-sized scalar mask atomically.
int getLayerCount() const noexcept
Current layer count, or zero before initialization.
~TerrainSplatmap()
Terrain splatmap.
Result< int > initialize(int width, int height, int layerCount, int defaultLayer=0)
Initialize topology and assign all weight to defaultLayer.
TerrainSplatmap & operator=(TerrainSplatmap &&) noexcept
Operator =.
Result< float > sample(int layer, int x, int y) const
Return one layer weight, or a structured range/precondition failure.
int getWidth() const noexcept
Current splat width, or zero before initialization.
int getLastChangedSamples() const noexcept
Number of texels changed by the last successful paint operation.
bool validRaster(const Heightmap &map)
Valid raster.
Result< TerrainMultiTileReport > paintTerrainSplatLayerMultiTile(const std::vector< TerrainSplatTile > &tiles, const Heightmap &operationPaint, int targetLayer, const TerrainStampSettings &operationSettings, bool worldMapOperation, const std::vector< std::string > &validTerrainNames)
Paint one shared Pcg Texture-domain operation raster across selected 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.
Result< int > paintTerrainSplatLayer(TerrainSplatmap &target, const Heightmap &paint, int targetLayer)
Replace one terrain layer with a normalized paint raster and proportionally renormalize all other lay...
Result< int > applyGtsHeightBlend(TerrainSplatmap &output, const TerrainSplatmap &input, const GtsHeightBlendSet &heights, float blendFactor)
Recompute all splat weights using the GTS texture-height blending equation.
Result< int > alignTerrainSplatTextures(TerrainSplatmap &terrainA, TerrainSplatmap &terrainB, const TerrainTextureAlignSettings &settings)
Blend texture weights on adjacent terrain edges using Pcg's synchronized-noise equation.
Completed multi-tile stamp statistics and Pcg-compatible affected-pixel mappings.
std::size_t index(int layer, int x, int y) const
Value configuration for a rectangular stamp in world X/Z coordinates.
World-space placement and Pcg-compatible seam blending controls for two splatmaps.
glm::vec4 bounds