载入中...
搜索中...
未找到
TerrainMesh.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <queue>
7
8namespace eve::procgen {
9namespace {
10float smoothstep(float a, float b, float v) {
11 const float t = std::clamp((v - a) / (b - a), 0.f, 1.f);
12 return t * t * (3.f - 2.f * t);
13}
14
15std::array<float, 3> normalAt(const Heightmap &hm, int x, int y, float cell, float heightScale) {
16 const int x0 = std::max(0, x - 1), x1 = std::min(hm.getWidth() - 1, x + 1);
17 const int y0 = std::max(0, y - 1), y1 = std::min(hm.getHeight() - 1, y + 1);
18 const float dx = (hm.height(x1, y) - hm.height(x0, y)) * heightScale /
19 (float(std::max(1, x1 - x0)) * cell);
20 const float dz = (hm.height(x, y1) - hm.height(x, y0)) * heightScale /
21 (float(std::max(1, y1 - y0)) * cell);
22 const float inv = 1.f / std::sqrt(dx * dx + 1.f + dz * dz);
23 return {-dx * inv, inv, -dz * inv};
24}
25
26std::array<float, 4> weightsAt(const Heightmap &hm, const TerrainLayers *layers, int x, int y,
27 const std::array<float, 3> &normal, float minH, float rangeH) {
28 std::array<float, 4> w{0.05f, 0.05f, 0.05f, 0.05f};
29 auto addBiome = [](std::array<float, 4> &target, Biome biome, float scale) {
30 switch (biome) {
32 target[0] += 0.9f * scale; break;
33 case Biome::River:
34 // Exposed mountain reaches are wet rock/riverbank rather than a
35 // bright sand stripe. Calm reaches receive a separate water mesh.
36 target[0] += 0.10f * scale;
37 target[1] += 0.34f * scale;
38 target[2] += 0.46f * scale;
39 break;
40 case Biome::Lake: target[0] += 0.75f * scale; target[1] += 0.15f * scale; break;
41 case Biome::Wetland: target[1] += 0.68f * scale; target[0] += 0.22f * scale; break;
43 target[1] += 0.9f * scale; break;
44 case Biome::Tundra: case Biome::Taiga:
45 target[3] += 0.75f * scale; target[1] += 0.15f * scale; break;
46 case Biome::Alpine: target[2] += 0.85f * scale; target[3] += 0.1f * scale; break;
47 }
48 };
49 if (layers) {
50 // Biomes remain categorical for gameplay, but their material response
51 // is a compact Gaussian blend. This removes chessboard boundaries at
52 // climate thresholds without washing out slope-driven rock or snow.
53 float kernelSum = 0.f;
54 for (int oy = -1; oy <= 1; ++oy) for (int ox = -1; ox <= 1; ++ox) {
55 const int sx = std::clamp(x + ox, 0, hm.getWidth() - 1);
56 const int sy = std::clamp(y + oy, 0, hm.getHeight() - 1);
57 const float kernel = float((ox == 0 ? 2 : 1) * (oy == 0 ? 2 : 1));
58 const int biome = std::clamp(layers->getBiome(sx, sy), 0, int(Biome::Wetland));
59 addBiome(w, Biome(biome), kernel);
60 kernelSum += kernel;
61 }
62 for (float &value : w) value /= kernelSum;
63 } else addBiome(w, Biome::Grassland, 1.f);
64 const float slope = 1.f - normal[1];
65 w[2] += smoothstep(0.08f, 0.42f, slope) * 1.2f;
66 if (layers) {
67 const float drainage = std::log1p(std::max(0.f, layers->getFlowAccumulation(x, y))) /
68 std::log1p(float(std::max(2, hm.getWidth() * hm.getHeight())));
69 const float lowGradient = 1.f - smoothstep(0.025f, 0.16f, slope);
70 const float alluvium = smoothstep(0.42f, 0.72f, drainage) * lowGradient;
71 // Mature, low-gradient drainage corridors expose silt, gravel and
72 // point-bar sediment even when the surrounding biome is humid. Without
73 // this counter-signal the widened floodplain is painted as one uniform
74 // green carpet and its geomorphic break in slope becomes invisible.
75 w[0] += alluvium * 0.72f;
76 w[2] += alluvium * 0.10f;
77 }
78 const float elevation = rangeH > 0.f ? (hm.height(x, y) - minH) / rangeH : 0.f;
79 w[2] += smoothstep(0.62f, 0.82f, elevation) * 0.35f;
80 w[3] += smoothstep(0.78f, 0.96f, elevation) * 0.65f;
81 if (layers) w[1] += layers->getMoisture(x, y) * (1.f - smoothstep(0.05f, 0.3f, slope)) * 0.25f;
82 const float sum = w[0] + w[1] + w[2] + w[3];
83 for (float &value : w) value /= sum;
84 return w;
85}
86
87std::vector<int> sampleAxis(int begin, int cells, int step) {
88 std::vector<int> axis;
89 for (int v = begin; v < begin + cells; v += step) axis.push_back(v);
90 axis.push_back(begin + cells);
91 return axis;
92}
93} // namespace
94
95float TerrainMeshChunk::getMaterialWeight(int vertex, int channel) const {
96 if (vertex < 0 || vertex >= getVertexCount() || channel < 0 || channel >= 4) return 0.f;
97 return weights_[size_t(vertex) * 4u + size_t(channel)];
98}
99
100int TerrainMeshChunk::getBiome(int vertex) const {
101 if (vertex < 0 || size_t(vertex) >= biomes_.size()) return -1;
102 return int(biomes_[size_t(vertex)]);
103}
104
107 std::string *error) {
108 if (s.cellsX <= 0 || s.cellsY <= 0 || s.originX < 0 || s.originY < 0 ||
109 s.originX + s.cellsX >= hm.getWidth() || s.originY + s.cellsY >= hm.getHeight() ||
110 s.lod < 0 || s.lod > 20 || !std::isfinite(s.cellSize) || s.cellSize <= 0.f ||
111 !std::isfinite(s.heightScale) || !std::isfinite(s.skirtDepth) || s.skirtDepth < 0.f) {
112 if (error) *error = "terrain mesh: invalid bounds or settings";
113 return false;
114 }
115 if (layers && (layers->getWidth() != hm.getWidth() || layers->getHeight() != hm.getHeight())) {
116 if (error) *error = "terrain mesh: layer dimensions do not match heightmap";
117 return false;
118 }
119 const int step = 1 << s.lod;
120 const std::vector<int> xs = sampleAxis(s.originX, s.cellsX, step);
121 const std::vector<int> ys = sampleAxis(s.originY, s.cellsY, step);
122 const auto [minIt, maxIt] = std::minmax_element(hm.data().begin(), hm.data().end());
123 const float minH = *minIt, rangeH = *maxIt - *minIt;
124 out = {}; out.lodStep_ = step; out.originX_ = s.originX; out.originY_ = s.originY;
125 out.geometricError_ = estimateGeometricError(hm, s);
126 const int cols = int(xs.size()), rows = int(ys.size());
127 out.splatWidth_ = cols; out.splatHeight_ = rows;
128 out.mesh_.reserve(cols * rows + (cols + rows) * 4, (cols - 1) * (rows - 1) * 6);
129 auto addVertex = [&](int gx, int gy, float yOffset, const std::array<float, 3> &n) {
130 out.mesh_.addVertex(float(gx - s.originX) * s.cellSize,
131 hm.height(gx, gy) * s.heightScale + yOffset,
132 float(gy - s.originY) * s.cellSize, n[0], n[1], n[2],
133 float(gx - s.originX) / float(s.cellsX),
134 float(gy - s.originY) / float(s.cellsY));
135 const auto weights = weightsAt(hm, layers, gx, gy, normalAt(hm, gx, gy, s.cellSize, s.heightScale), minH, rangeH);
136 out.weights_.insert(out.weights_.end(), weights.begin(), weights.end());
137 out.biomes_.push_back(uint8_t(layers ? std::clamp(layers->getBiome(gx, gy), 0, int(Biome::Wetland))
138 : int(Biome::Grassland)));
139 };
140 for (int gy : ys) for (int gx : xs) addVertex(gx, gy, 0.f, normalAt(hm, gx, gy, s.cellSize, s.heightScale));
141 out.baseVertexCount_ = out.mesh_.getVertexCount();
142 for (int y = 0; y + 1 < rows; ++y) for (int x = 0; x + 1 < cols; ++x) {
143 const uint32_t a = uint32_t(y * cols + x), b = a + 1, c = a + uint32_t(cols), d = c + 1;
144 out.mesh_.addTriangle(a, c, b); out.mesh_.addTriangle(b, c, d);
145 }
146
147 if (s.skirtDepth > 0.f) {
148 struct Edge { std::vector<std::pair<int, int>> points; std::array<float, 3> normal; };
149 std::array<Edge, 4> edges;
150 for (int gx : xs) edges[0].points.emplace_back(gx, ys.front());
151 for (int gy : ys) edges[1].points.emplace_back(xs.back(), gy);
152 for (auto it = xs.rbegin(); it != xs.rend(); ++it) edges[2].points.emplace_back(*it, ys.back());
153 for (auto it = ys.rbegin(); it != ys.rend(); ++it) edges[3].points.emplace_back(xs.front(), *it);
154 edges[0].normal = {0.f, 0.f, -1.f}; edges[1].normal = {1.f, 0.f, 0.f};
155 edges[2].normal = {0.f, 0.f, 1.f}; edges[3].normal = {-1.f, 0.f, 0.f};
156 for (const Edge &edge : edges) {
157 const uint32_t start = uint32_t(out.mesh_.getVertexCount());
158 for (const auto &[gx, gy] : edge.points) {
159 addVertex(gx, gy, 0.f, edge.normal); addVertex(gx, gy, -s.skirtDepth, edge.normal);
160 }
161 for (uint32_t i = 0; i + 1 < edge.points.size(); ++i) {
162 const uint32_t topA = start + i * 2, bottomA = topA + 1, topB = topA + 2, bottomB = topB + 1;
163 out.mesh_.addTriangle(topA, topB, bottomA); out.mesh_.addTriangle(topB, bottomB, bottomA);
164 }
165 }
166 }
167 out.mesh_.setMeta("kind", "terrain.chunk");
168 out.mesh_.setMeta("lod", std::to_string(s.lod));
169 out.mesh_.setMeta("originX", std::to_string(s.originX));
170 out.mesh_.setMeta("originY", std::to_string(s.originY));
171 return true;
172}
173
175 const TerrainMeshSettings &s) {
176 if (s.lod <= 0 || s.originX < 0 || s.originY < 0 || s.cellsX <= 0 || s.cellsY <= 0 ||
177 s.originX + s.cellsX >= hm.getWidth() || s.originY + s.cellsY >= hm.getHeight() ||
178 !std::isfinite(s.heightScale)) return 0.f;
179 const int step = 1 << std::min(s.lod, 20);
180 float maxError = 0.f;
181 for (int by = 0; by < s.cellsY; by += step) {
182 const int y0 = s.originY + by;
183 const int y1 = s.originY + std::min(by + step, s.cellsY);
184 for (int bx = 0; bx < s.cellsX; bx += step) {
185 const int x0 = s.originX + bx;
186 const int x1 = s.originX + std::min(bx + step, s.cellsX);
187 const float h00 = hm.height(x0, y0), h10 = hm.height(x1, y0);
188 const float h01 = hm.height(x0, y1), h11 = hm.height(x1, y1);
189 for (int y = y0; y <= y1; ++y) for (int x = x0; x <= x1; ++x) {
190 const float tx = x1 > x0 ? float(x - x0) / float(x1 - x0) : 0.f;
191 const float ty = y1 > y0 ? float(y - y0) / float(y1 - y0) : 0.f;
192 const float approximation = std::lerp(std::lerp(h00, h10, tx),
193 std::lerp(h01, h11, tx), ty);
194 maxError = std::max(maxError,
195 std::abs(hm.height(x, y) - approximation) * std::abs(s.heightScale));
196 }
197 }
198 }
199 return maxError;
200}
201
203 float distanceToCamera, float viewportHeight,
204 float verticalFovDegrees, float targetPixelError) {
205 if (maxLod < 0 || maxLod > 20 || !std::isfinite(distanceToCamera) || distanceToCamera <= 0.f ||
206 !std::isfinite(viewportHeight) || viewportHeight <= 0.f ||
207 !std::isfinite(verticalFovDegrees) || verticalFovDegrees <= 1.f || verticalFovDegrees >= 179.f ||
208 !std::isfinite(targetPixelError) || targetPixelError <= 0.f ||
209 s.originX < 0 || s.originY < 0 || s.cellsX <= 0 || s.cellsY <= 0 ||
210 s.originX + s.cellsX >= hm.getWidth() || s.originY + s.cellsY >= hm.getHeight() ||
211 !std::isfinite(s.heightScale)) return -1;
212 const float radians = verticalFovDegrees * 0.01745329251994329577f;
213 const float projectionScale = viewportHeight / (2.f * std::tan(radians * 0.5f));
214 for (int lod = maxLod; lod >= 0; --lod) {
215 s.lod = lod;
217 projectionScale / distanceToCamera;
218 if (projected <= targetPixelError) return lod;
219 }
220 return 0;
221}
222
225 std::string *error) {
226 if (s.cellsX <= 0 || s.cellsY <= 0 || s.originX < 0 || s.originY < 0 ||
227 s.originX + s.cellsX >= hm.getWidth() || s.originY + s.cellsY >= hm.getHeight() ||
228 layers.getWidth() != hm.getWidth() || layers.getHeight() != hm.getHeight() ||
229 !std::isfinite(s.cellSize) || s.cellSize <= 0.f ||
230 !std::isfinite(s.heightScale) || !std::isfinite(s.minWidth) ||
231 !std::isfinite(s.maxWidth) || s.minWidth <= 0.f || s.maxWidth < s.minWidth ||
232 !std::isfinite(s.heightOffset) || !std::isfinite(s.minSurfaceSlope) ||
233 !std::isfinite(s.maxSurfaceSlope) || s.minSurfaceSlope < 0.f ||
234 s.maxSurfaceSlope <= s.minSurfaceSlope) {
235 if (error) *error = "terrain river mesh: invalid bounds or settings";
236 return false;
237 }
238 out = {};
239 static constexpr std::array<int, 8> flowDx{-1, 0, 1, -1, 1, -1, 0, 1};
240 static constexpr std::array<int, 8> flowDy{-1, -1, -1, 0, 0, 1, 1, 1};
241 const size_t layerCells = size_t(layers.getWidth()) * size_t(layers.getHeight());
242 std::vector<float> channelFlow(layerCells, 0.f);
243 std::vector<uint16_t> riverIndegree(layerCells, 0);
244 for (int y = 0; y < layers.getHeight(); ++y) for (int x = 0; x < layers.getWidth(); ++x) {
245 const size_t i = size_t(y * layers.getWidth() + x);
246 if (!layers.isRiver(x, y)) continue;
247 channelFlow[i] = std::max(0.f, layers.getFlowAccumulation(x, y));
248 const int direction = layers.getFlowDirection(x, y);
249 if (direction < 0 || direction >= 8) continue;
250 const int rx = x + flowDx[size_t(direction)], ry = y + flowDy[size_t(direction)];
251 if (rx >= 0 && ry >= 0 && rx < layers.getWidth() && ry < layers.getHeight() &&
252 layers.isRiver(rx, ry)) ++riverIndegree[size_t(ry * layers.getWidth() + rx)];
253 }
254 std::queue<size_t> riverQueue;
255 for (size_t i = 0; i < layerCells; ++i)
256 if (channelFlow[i] > 0.f && riverIndegree[i] == 0) riverQueue.push(i);
257 while (!riverQueue.empty()) {
258 const size_t i = riverQueue.front(); riverQueue.pop();
259 const int x = int(i % size_t(layers.getWidth()));
260 const int y = int(i / size_t(layers.getWidth()));
261 const int direction = layers.getFlowDirection(x, y);
262 if (direction < 0 || direction >= 8) continue;
263 const int rx = x + flowDx[size_t(direction)], ry = y + flowDy[size_t(direction)];
264 if (rx < 0 || ry < 0 || rx >= layers.getWidth() || ry >= layers.getHeight() ||
265 !layers.isRiver(rx, ry)) continue;
266 const size_t receiver = size_t(ry * layers.getWidth() + rx);
267 channelFlow[receiver] = std::max(channelFlow[receiver], channelFlow[i]);
268 if (riverIndegree[receiver] > 0 && --riverIndegree[receiver] == 0)
269 riverQueue.push(receiver);
270 }
271 float maxFlow = 1.f;
272 for (float flow : channelFlow) maxFlow = std::max(maxFlow, flow);
273 std::vector<uint8_t> calmReach(layerCells, 0);
274 for (int y = 0; y < layers.getHeight(); ++y) for (int x = 0; x < layers.getWidth(); ++x) {
275 const int d = layers.getFlowDirection(x, y);
276 if (!layers.isRiver(x, y) || d < 0 || d >= 8) continue;
277 const int rx = x + flowDx[size_t(d)], ry = y + flowDy[size_t(d)];
278 if (rx < 0 || ry < 0 || rx >= hm.getWidth() || ry >= hm.getHeight()) continue;
279 const float run = s.cellSize * std::sqrt(float(flowDx[size_t(d)] * flowDx[size_t(d)] +
280 flowDy[size_t(d)] * flowDy[size_t(d)]));
281 const float rise = std::abs(hm.height(rx, ry) - hm.height(x, y)) * s.heightScale;
282 const float grade = rise / run;
283 calmReach[size_t(y * layers.getWidth() + x)] =
284 uint8_t(grade >= s.minSurfaceSlope && grade < s.maxSurfaceSlope);
285 }
286 // Close isolated one-cell grade spikes only when a calm upstream segment
287 // and calm downstream segment both exist. Long cascades remain suppressed.
288 std::vector<uint8_t> closedReach = calmReach;
289 for (int y = 1; y + 1 < layers.getHeight(); ++y) for (int x = 1; x + 1 < layers.getWidth(); ++x) {
290 const size_t i = size_t(y * layers.getWidth() + x);
291 const int d = layers.getFlowDirection(x, y);
292 if (calmReach[i] || !layers.isRiver(x, y) || d < 0 || d >= 8) continue;
293 const int rx = x + flowDx[size_t(d)], ry = y + flowDy[size_t(d)];
294 const size_t receiver = size_t(ry * layers.getWidth() + rx);
295 if (!calmReach[receiver]) continue;
296 bool calmDonor = false;
297 for (int incoming = 0; incoming < 8 && !calmDonor; ++incoming) {
298 const int nx = x + flowDx[size_t(incoming)], ny = y + flowDy[size_t(incoming)];
299 const int nd = layers.getFlowDirection(nx, ny);
300 if (nd < 0 || nd >= 8) continue;
301 const size_t n = size_t(ny * layers.getWidth() + nx);
302 calmDonor = calmReach[n] && nx + flowDx[size_t(nd)] == x &&
303 ny + flowDy[size_t(nd)] == y;
304 }
305 if (calmDonor) closedReach[i] = 1;
306 }
307 auto strongestDonor = [&](int sx, int sy) {
308 float donorFlow = -1.f;
309 int donorX = sx, donorY = sy;
310 for (int candidate = 0; candidate < 8; ++candidate) {
311 const int dxCell = sx + flowDx[size_t(candidate)];
312 const int dyCell = sy + flowDy[size_t(candidate)];
313 if (dxCell < 0 || dyCell < 0 || dxCell >= layers.getWidth() ||
314 dyCell >= layers.getHeight()) continue;
315 const int donorDirection = layers.getFlowDirection(dxCell, dyCell);
316 if (donorDirection < 0 || donorDirection >= 8 ||
317 dxCell + flowDx[size_t(donorDirection)] != sx ||
318 dyCell + flowDy[size_t(donorDirection)] != sy) continue;
319 const float flow = layers.getFlowAccumulation(dxCell, dyCell);
320 if (flow > donorFlow) {
321 donorFlow = flow; donorX = dxCell; donorY = dyCell;
322 }
323 }
324 return std::array<int, 2>{donorX, donorY};
325 };
326 auto receiver = [&](int sx, int sy) {
327 const int direction = layers.getFlowDirection(sx, sy);
328 if (direction < 0 || direction >= 8) return std::array<int, 2>{sx, sy};
329 const int receiverX = sx + flowDx[size_t(direction)];
330 const int receiverY = sy + flowDy[size_t(direction)];
331 if (receiverX < 0 || receiverY < 0 || receiverX >= layers.getWidth() ||
332 receiverY >= layers.getHeight()) return std::array<int, 2>{sx, sy};
333 return std::array<int, 2>{receiverX, receiverY};
334 };
335 auto smoothCenter = [&](int sx, int sy) {
336 // Binomial five-point filtering follows the highest-discharge donor
337 // through the cell and two receivers downstream. It suppresses the
338 // one-cell D8 staircase while retaining confluences and is deterministic
339 // across independently generated chunks. The weights sum to one and
340 // preserve straight reaches exactly.
341 const auto pMinus1 = strongestDonor(sx, sy);
342 const auto pMinus2 = strongestDonor(pMinus1[0], pMinus1[1]);
343 const auto pPlus1 = receiver(sx, sy);
344 const auto pPlus2 = receiver(pPlus1[0], pPlus1[1]);
345 return std::array<float, 2>{
346 (float(pMinus2[0]) + 4.f * float(pMinus1[0]) + 6.f * float(sx) +
347 4.f * float(pPlus1[0]) + float(pPlus2[0])) / 16.f,
348 (float(pMinus2[1]) + 4.f * float(pMinus1[1]) + 6.f * float(sy) +
349 4.f * float(pPlus1[1]) + float(pPlus2[1])) / 16.f};
350 };
351 auto sampleHeight = [&](float sx, float sy) {
352 sx = std::clamp(sx, 0.f, float(hm.getWidth() - 1));
353 sy = std::clamp(sy, 0.f, float(hm.getHeight() - 1));
354 const int x0 = int(std::floor(sx)), y0 = int(std::floor(sy));
355 const int x1 = std::min(x0 + 1, hm.getWidth() - 1);
356 const int y1 = std::min(y0 + 1, hm.getHeight() - 1);
357 const float tx = sx - float(x0), ty = sy - float(y0);
358 return std::lerp(std::lerp(hm.height(x0, y0), hm.height(x1, y0), tx),
359 std::lerp(hm.height(x0, y1), hm.height(x1, y1), tx), ty);
360 };
361 for (int y = s.originY; y <= s.originY + s.cellsY; ++y) {
362 for (int x = s.originX; x <= s.originX + s.cellsX; ++x) {
363 const int direction = layers.getFlowDirection(x, y);
364 if (!layers.isRiver(x, y) || direction < 0 || direction >= 8) continue;
365 if (!closedReach[size_t(y * layers.getWidth() + x)]) continue;
366 const int receiverX = x + flowDx[size_t(direction)];
367 const int receiverY = y + flowDy[size_t(direction)];
368 const auto startCenter = smoothCenter(x, y);
369 const auto endCenter = smoothCenter(receiverX, receiverY);
370 const float tx0 = endCenter[0] - startCenter[0];
371 const float tz0 = endCenter[1] - startCenter[1];
372 const float tangentLengthSquared = tx0 * tx0 + tz0 * tz0;
373 if (tangentLengthSquared <= 1e-8f) continue;
374 const float invLength = 1.f / std::sqrt(tangentLengthSquared);
375 const float tx = tx0 * invLength, tz = tz0 * invLength;
376 auto widthAt = [&](int sampleX, int sampleY) {
377 // Downstream hydraulic geometry is commonly approximated by a
378 // discharge power law. The 0.36 exponent retains readable head
379 // streams while producing gradual main-stem widening.
380 const float discharge = channelFlow[size_t(sampleY * layers.getWidth() + sampleX)];
381 const float areaScale = maxFlow > 1.f
382 ? std::pow(std::clamp(discharge / maxFlow, 0.f, 1.f), 0.36f)
383 : 0.f;
384 const float orderScale = std::clamp(
385 (float(layers.getStreamOrder(sampleX, sampleY)) - 1.f) / 4.f, 0.f, 1.f);
386 const float flowScale = std::max(areaScale, orderScale * 0.82f);
387 return 0.5f * std::lerp(s.minWidth, s.maxWidth, flowScale);
388 };
389 const float startWidth = widthAt(x, y);
390 const float endWidth = layers.isRiver(receiverX, receiverY)
391 ? widthAt(receiverX, receiverY) : startWidth;
392 float startFlowX = layers.getFlowVectorX(x, y);
393 float startFlowY = layers.getFlowVectorY(x, y);
394 float endFlowX = layers.getFlowVectorX(receiverX, receiverY);
395 float endFlowY = layers.getFlowVectorY(receiverX, receiverY);
396 if (std::hypot(startFlowX, startFlowY) < 0.1f) {
397 startFlowX = tx; startFlowY = tz;
398 }
399 if (std::hypot(endFlowX, endFlowY) < 0.1f) {
400 endFlowX = tx; endFlowY = tz;
401 }
402 const float chordCells = std::sqrt(tangentLengthSquared);
403 auto curvePoint = [&](float t) {
404 const float t2 = t * t, t3 = t2 * t;
405 const float h00 = 2.f * t3 - 3.f * t2 + 1.f;
406 const float h10 = t3 - 2.f * t2 + t;
407 const float h01 = -2.f * t3 + 3.f * t2;
408 const float h11 = t3 - t2;
409 return std::array<float, 2>{
410 h00 * startCenter[0] + h10 * startFlowX * chordCells +
411 h01 * endCenter[0] + h11 * endFlowX * chordCells,
412 h00 * startCenter[1] + h10 * startFlowY * chordCells +
413 h01 * endCenter[1] + h11 * endFlowY * chordCells};
414 };
415 constexpr int curveSegments = 5;
416 std::array<std::array<float, 2>, curveSegments + 1> curve{};
417 for (int point = 0; point <= curveSegments; ++point)
418 curve[size_t(point)] = curvePoint(float(point) / float(curveSegments));
419 const uint32_t stripStart = uint32_t(out.getVertexCount());
420 for (int point = 0; point <= curveSegments; ++point) {
421 const int previous = std::max(0, point - 1);
422 const int following = std::min(curveSegments, point + 1);
423 const float qdx = curve[size_t(following)][0] - curve[size_t(previous)][0];
424 const float qdy = curve[size_t(following)][1] - curve[size_t(previous)][1];
425 const float t = float(point) / float(curveSegments);
426 // Every independently emitted reach that meets at a drainage
427 // cell must produce the same bank pair at that cell. Using the
428 // finite-difference tangent of each Hermite span made the two
429 // reaches disagree at their shared endpoint, leaving the
430 // scalloped/saw-tooth silhouette visible in rendered rivers.
431 // The stored continuous flow vector is cell-local and therefore
432 // identical on both sides of the join. Blend it inside the span,
433 // but snap endpoint frames exactly to the corresponding cell.
434 float frameX = std::lerp(startFlowX, endFlowX, t);
435 float frameY = std::lerp(startFlowY, endFlowY, t);
436 if (point == 0) { frameX = startFlowX; frameY = startFlowY; }
437 if (point == curveSegments) { frameX = endFlowX; frameY = endFlowY; }
438 float frameLength = std::hypot(frameX, frameY);
439 if (frameLength < 1e-5f) {
440 frameX = qdx; frameY = qdy;
441 frameLength = std::max(1e-5f, std::hypot(frameX, frameY));
442 }
443 const float qpx = -frameY / frameLength;
444 const float qpz = frameX / frameLength;
445 const float width = std::lerp(startWidth, endWidth, t);
446 const auto &q = curve[size_t(point)];
447 const float qx = (q[0] - float(s.originX)) * s.cellSize;
448 const float qz = (q[1] - float(s.originY)) * s.cellSize;
449 const float widthCells = width / s.cellSize;
450 const float centerBed = sampleHeight(q[0], q[1]);
451 const float leftBed = sampleHeight(q[0] - qpx * widthCells,
452 q[1] - qpz * widthCells);
453 const float rightBed = sampleHeight(q[0] + qpx * widthCells,
454 q[1] + qpz * widthCells);
455 // One transverse water level must clear both banks. Sampling
456 // only the center buried the uphill half of ribbons whenever
457 // a smoothed centerline crossed a sloping raster triangle,
458 // which appeared as broken rectangular river segments.
459 const float qy = std::max({centerBed, leftBed, rightBed}) *
460 s.heightScale + s.heightOffset;
461 out.addVertex(qx - qpx * width, qy, qz - qpz * width,
462 0.f, 1.f, 0.f, 0.f, t);
463 out.addVertex(qx + qpx * width, qy, qz + qpz * width,
464 0.f, 1.f, 0.f, 1.f, t);
465 }
466 for (int segment = 0; segment < curveSegments; ++segment) {
467 const uint32_t a = stripStart + uint32_t(segment * 2);
468 const uint32_t b = a + 1u, c = a + 2u, dVertex = a + 3u;
469 out.addTriangle(a, c, b);
470 out.addTriangle(b, c, dVertex);
471 }
472 }
473 }
474 out.setMeta("kind", "terrain.rivers");
475 return true;
476}
477
480 std::string *error) {
481 if (s.cellsX <= 0 || s.cellsY <= 0 || s.originX < 0 || s.originY < 0 ||
482 s.originX + s.cellsX >= hm.getWidth() || s.originY + s.cellsY >= hm.getHeight() ||
483 layers.getWidth() != hm.getWidth() || layers.getHeight() != hm.getHeight() ||
484 !std::isfinite(s.cellSize) || s.cellSize <= 0.f ||
485 !std::isfinite(s.heightScale) || !std::isfinite(s.minimumDepth) ||
486 s.minimumDepth < 0.f || !std::isfinite(s.heightOffset)) {
487 if (error) *error = "terrain lake mesh: invalid bounds or settings";
488 return false;
489 }
490 out = {};
491 struct ShoreVertex { float x, y, z, depth; };
492 for (int y = s.originY; y < s.originY + s.cellsY; ++y) {
493 for (int x = s.originX; x < s.originX + s.cellsX; ++x) {
494 const float d00 = layers.getLakeDepth(x, y);
495 const float d10 = layers.getLakeDepth(x + 1, y);
496 const float d01 = layers.getLakeDepth(x, y + 1);
497 const float d11 = layers.getLakeDepth(x + 1, y + 1);
498 if (std::max({d00, d10, d01, d11}) < s.minimumDepth) continue;
499 auto surface = [&](int sx, int sy, float depth) {
500 return (hm.height(sx, sy) + depth) * s.heightScale + s.heightOffset;
501 };
502 const float x0 = float(x - s.originX) * s.cellSize;
503 const float x1 = float(x + 1 - s.originX) * s.cellSize;
504 const float z0 = float(y - s.originY) * s.cellSize;
505 const float z1 = float(y + 1 - s.originY) * s.cellSize;
506 // Clip the cell polygon against the requested depth contour. This
507 // is the filled equivalent of marching squares and places shoreline
508 // vertices inside cells instead of producing staircase coastlines.
509 std::vector<ShoreVertex> polygon{
510 {x0, surface(x, y, d00), z0, d00},
511 {x1, surface(x + 1, y, d10), z0, d10},
512 {x1, surface(x + 1, y + 1, d11), z1, d11},
513 {x0, surface(x, y + 1, d01), z1, d01},
514 };
515 std::vector<ShoreVertex> clipped;
516 for (size_t edge = 0; edge < polygon.size(); ++edge) {
517 const ShoreVertex &a = polygon[edge];
518 const ShoreVertex &b = polygon[(edge + 1) % polygon.size()];
519 const bool aInside = a.depth >= s.minimumDepth;
520 const bool bInside = b.depth >= s.minimumDepth;
521 if (aInside) clipped.push_back(a);
522 if (aInside == bInside) continue;
523 const float t = std::clamp((s.minimumDepth - a.depth) /
524 (b.depth - a.depth), 0.f, 1.f);
525 clipped.push_back({std::lerp(a.x, b.x, t), std::lerp(a.y, b.y, t),
526 std::lerp(a.z, b.z, t), s.minimumDepth});
527 }
528 if (clipped.size() < 3) continue;
529 const uint32_t first = uint32_t(out.getVertexCount());
530 for (const ShoreVertex &v : clipped) {
531 const float u = (v.x / (float(s.cellsX) * s.cellSize));
532 const float texV = (v.z / (float(s.cellsY) * s.cellSize));
533 out.addVertex(v.x, v.y, v.z, 0.f, 1.f, 0.f, u, texV);
534 }
535 for (uint32_t vertex = 1; vertex + 1 < clipped.size(); ++vertex)
536 out.addTriangle(first, first + vertex + 1, first + vertex);
537 }
538 }
539 out.setMeta("kind", "terrain.lakes");
540 return true;
541}
542
543} // namespace eve::procgen
LogicalId target
double value
Duration start
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
const std::string & s
Vec3 projected
Definition CaveMesh.cpp:122
int bx
Definition CaveMesh.cpp:114
int by
Definition CaveMesh.cpp:114
float nx
float ny
int rows
int cols
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
std::array< double, 10 > q
float v
std::int32_t c
std::int32_t first
float elevation
int biome
std::uint32_t width
std::array< float, 3 > scale
double maxError
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
Texture * normal
graphics::Canvas * previous
std::weak_ptr< Run > run
Definition OnnxGpgpu.cpp:25
std::string error
Definition Package.cpp:60
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::shared_ptr< const std::vector< glm::vec2 > > points
float begin
float d
float t
int lod
RoadLaneDirection direction
const RoadEdge * edge
float dz
float dx
Cell cell
Heightmap curve
float weights[3]
float step
Definition TreeMesh.cpp:314
std::uint32_t depth
double oy
double ox
std::vector< int > edges
glm::vec3 point
float frameY
float frameX
float qy
float qx
float qz
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
CPU triangle mesh from procedural mesh recipes (e.g. marching cubes). Positions/normals are xyz-packe...
Definition MeshBuild.h:19
void addVertex(float px, float py, float pz, float nx, float ny, float nz, float u, float v)
Adds vertex.
Definition MeshBuild.cpp:33
void addTriangle(uint32_t i0, uint32_t i1, uint32_t i2)
Adds triangle.
Definition MeshBuild.cpp:46
void reserve(int vertexCount, int indexCount)
Reserve.
Definition MeshBuild.cpp:20
void setMeta(const std::string &key, const std::string &value)
Sets the meta.
static bool build(const Heightmap &heightmap, const TerrainLayers &layers, const TerrainLakeMeshSettings &settings, MeshBuild &out, std::string *error=nullptr)
Compatibility operation that builds lake surface geometry.
Script-friendly ownership wrapper for baked hydrology and climate layers.
int getStreamOrder(int x, int y) const
Return Strahler river order, or zero outside the river network.
int getBiome(int x, int y) const
Returns the biome.
int getWidth() const
Returns the width.
int getHeight() const
Returns the height.
bool isRiver(int x, int y) const
True when river.
int getFlowDirection(int x, int y) const
Return the D8 receiver direction for one cell, or -1 at an outlet/out of bounds.
float getFlowAccumulation(int x, int y) const
Returns the flow accumulation.
float getFlowVectorY(int x, int y) const
Return the continuous normalized downslope Y component.
float getLakeDepth(int x, int y) const
Return filled-depression water depth, or zero outside a lake.
float getFlowVectorX(int x, int y) const
Return the continuous normalized downslope X component.
static int select(const Heightmap &heightmap, TerrainMeshSettings settings, int maxLod, float distance, float viewportHeight, float verticalFovDegrees, float targetPixelError)
Select the coarsest LOD whose projected geometric error is within budget.
static bool build(const Heightmap &heightmap, const TerrainLayers *layers, const TerrainMeshSettings &settings, TerrainMeshChunk &out, std::string *error=nullptr)
Compatibility operation that builds a terrain mesh chunk.
static float estimateGeometricError(const Heightmap &heightmap, const TerrainMeshSettings &settings)
Maximum world-space deviation between a LOD grid and the source heightfield.
One renderable heightfield chunk plus four ecological material weights.
Definition TerrainMesh.h:27
int getBiome(int vertex) const
Return the biome id stored for one mesh vertex, or -1 when out of range.
float getMaterialWeight(int vertex, int channel) const
Returns the material weight.
int getVertexCount() const
Returns the vertex count.
Definition TerrainMesh.h:34
static bool build(const Heightmap &heightmap, const TerrainLayers &layers, const TerrainRiverMeshSettings &settings, MeshBuild &out, std::string *error=nullptr)
Compatibility operation that builds river surface geometry.
Biome
Biome public API.
int axis(int64_t a, size_t rank)
Axis.
TerrainLakeMeshSettings public API.
TerrainMeshSettings public API.
Definition TerrainMesh.h:12
TerrainRiverMeshSettings public API.
Definition TerrainMesh.h:91