载入中...
搜索中...
未找到
UrbanCrossField.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <cmath>
7#include <cstdint>
8#include <numeric>
9#include <utility>
10#include <vector>
11
13namespace {
14
15constexpr double kPi = 3.14159265358979323846;
16
18struct CrossField {
19 int nx = 0;
20 int ny = 0;
21 double ox = 0.0; // origin
22 double oy = 0.0;
23 double spacing = 1.0;
24 std::vector<double> phi; // 2θ in radians, wrapped to [-π, π)
25 std::vector<char> inside;
26
27 bool cellInside(int x, int y) const {
28 if (x < 0 || y < 0 || x >= nx || y >= ny) return false;
29 return inside[size_t(y) * size_t(nx) + size_t(x)] != 0;
30 }
31
32 bool cellInsideClamped(int x, int y, int* cx, int* cy) const {
33 const int xx = std::clamp(x, 0, nx - 1);
34 const int yy = std::clamp(y, 0, ny - 1);
35 if (cx) *cx = xx;
36 if (cy) *cy = yy;
37 return inside[size_t(yy) * size_t(nx) + size_t(xx)] != 0;
38 }
39
41 double thetaAt(const Vec2& p) const {
42 const double fx = (p.x - ox) / spacing;
43 const double fy = (p.y - oy) / spacing;
44 const int x0 = std::clamp(int(std::floor(fx)), 0, nx - 1);
45 const int y0 = std::clamp(int(std::floor(fy)), 0, ny - 1);
46 const int x1 = std::min(x0 + 1, nx - 1);
47 const int y1 = std::min(y0 + 1, ny - 1);
48 const double tx = std::clamp(fx - double(x0), 0.0, 1.0);
49 const double ty = std::clamp(fy - double(y0), 0.0, 1.0);
50 // Wrapped bilinear interpolation via unit vectors.
51 const int ix[2] = {x0, x1};
52 const int iy[2] = {y0, y1};
53 Vec2 avg{0, 0};
54 for (int j = 0; j < 2; ++j) {
55 for (int i = 0; i < 2; ++i) {
56 const int cx = ix[i];
57 const int cy = iy[j];
58 double a = 0.0;
59 if (cellInside(cx, cy)) {
60 a = phi[size_t(cy) * size_t(nx) + size_t(cx)];
61 } else {
62 int ccx = cx, ccy = cy;
63 int guard = 0;
64 while (!cellInside(ccx, ccy) && guard++ < 64) {
65 // Cheap fallback: search a small spiral for the nearest inside cell.
66 bool found = false;
67 for (int r = 1; r <= 4; ++r) {
68 for (int dy = -r; dy <= r && !found; ++dy) {
69 for (int dx = -r; dx <= r && !found; ++dx) {
70 if (cellInside(cx + dx, cy + dy)) {
71 ccx = cx + dx;
72 ccy = cy + dy;
73 found = true;
74 }
75 }
76 }
77 if (found) break;
78 }
79 if (!found) {
80 ccx = cx;
81 ccy = cy;
82 break;
83 }
84 }
85 a = phi[size_t(ccy) * size_t(nx) + size_t(ccx)];
86 }
87 const double w = (i == 0 ? 1.0 - tx : tx) * (j == 0 ? 1.0 - ty : ty);
88 avg = avg + Vec2{std::cos(a), std::sin(a)} * w;
89 }
90 }
91 double a = std::atan2(avg.y, avg.x);
92 if (a < 0) a += 2.0 * kPi;
93 return a * 0.5; // θ = φ/2
94 }
95};
96
98CrossField buildCrossField(const Polygon& poly, double spacing) {
99 double minX = poly[0].x, minY = poly[0].y, maxX = poly[0].x, maxY = poly[0].y;
100 for (const Vec2& p : poly) {
101 minX = std::min(minX, p.x);
102 minY = std::min(minY, p.y);
103 maxX = std::max(maxX, p.x);
104 maxY = std::max(maxY, p.y);
105 }
106 const double margin = spacing * 0.5;
107 minX -= margin;
108 minY -= margin;
109 maxX += margin;
110 maxY += margin;
111 CrossField f;
112 f.spacing = spacing;
113 f.ox = minX;
114 f.oy = minY;
115 f.nx = std::max(2, int(std::ceil((maxX - minX) / spacing)));
116 f.ny = std::max(2, int(std::ceil((maxY - minY) / spacing)));
117 f.phi.assign(size_t(f.nx) * size_t(f.ny), 0.0);
118 f.inside.assign(size_t(f.nx) * size_t(f.ny), 0);
119
120 // Initialize: nearest-boundary tangent angle (θ mod π → φ = 2θ).
121 for (int y = 0; y < f.ny; ++y) {
122 for (int x = 0; x < f.nx; ++x) {
123 const Vec2 c{f.ox + (double(x) + 0.5) * spacing, f.oy + (double(y) + 0.5) * spacing};
124 if (!pointInPolygon(c, poly)) continue;
125 f.inside[size_t(y) * size_t(f.nx) + size_t(x)] = 1;
126 BoundaryPosition pos;
128 // Tangent of the closest boundary edge.
129 const size_t n = poly.size();
130 const Vec2& a = poly[size_t(pos.edgeIndex)];
131 const Vec2& b = poly[size_t((pos.edgeIndex + 1) % int(n))];
132 const double tangent = std::atan2(b.y - a.y, b.x - a.x);
133 double phi = std::fmod(2.0 * tangent, 2.0 * kPi);
134 if (phi < 0) phi += 2.0 * kPi;
135 f.phi[size_t(y) * size_t(f.nx) + size_t(x)] = phi;
136 }
137 }
138
139 // Smooth the wrapped angle field with Jacobi iterations.
140 const int iterations = 240;
141 std::vector<double> next(f.phi.size(), 0.0);
142 for (int it = 0; it < iterations; ++it) {
143 for (int y = 0; y < f.ny; ++y) {
144 for (int x = 0; x < f.nx; ++x) {
145 const size_t idx = size_t(y) * size_t(f.nx) + size_t(x);
146 if (!f.inside[idx]) continue;
147 Vec2 sum{0, 0};
148 int count = 0;
149 const int dx[4] = {1, -1, 0, 0};
150 const int dy[4] = {0, 0, 1, -1};
151 for (int d = 0; d < 4; ++d) {
152 const int nx = x + dx[d];
153 const int ny = y + dy[d];
154 if (!f.cellInside(nx, ny)) continue;
155 const double a = f.phi[size_t(ny) * size_t(f.nx) + size_t(nx)];
156 sum = sum + Vec2{std::cos(a), std::sin(a)};
157 ++count;
158 }
159 if (count > 0) {
160 double a = std::atan2(sum.y, sum.x);
161 if (a < 0) a += 2.0 * kPi;
162 next[idx] = a;
163 } else {
164 next[idx] = f.phi[idx];
165 }
166 }
167 }
168 f.phi.swap(next);
169 }
170 return f;
171}
172
177Polyline traceStreamline(const CrossField& field, const Polygon& poly, const Vec2& seed, const Vec2& initialDir,
178 double spacing, int maxSteps) {
179 Polyline out;
180 out.push_back(seed);
181 Vec2 p = seed;
182 Vec2 d = normalize(initialDir);
183 const double step = spacing * 0.5;
184 const double boundaryTol = spacing * 0.75;
185 for (int s = 0; s < maxSteps; ++s) {
186 const double theta = field.thetaAt(p);
187 const double c = std::cos(theta);
188 const double sn = std::sin(theta);
189 const Vec2 axes[2] = {{c, sn}, {-sn, c}};
190 // Choose the axis (and sign) best aligned with the current direction.
191 double bestScore = -1.0;
192 Vec2 bestDir = d;
193 for (int i = 0; i < 2; ++i) {
194 for (int sgn = -1; sgn <= 1; sgn += 2) {
195 const Vec2 cand = axes[i] * double(sgn);
196 const double sc = std::fabs(dot(cand, d));
197 if (sc > bestScore) {
198 bestScore = sc;
199 bestDir = cand;
200 }
201 }
202 }
203 d = bestDir;
204 Vec2 np = p + d * step;
205 if (!pointInPolygon(np, poly)) {
206 // Hit the boundary: project back onto it and finish.
207 BoundaryPosition pos;
208 const Vec2 hit = closestPointOnBoundary(poly, np, &pos);
209 if (distance(hit, out.back()) > 1e-9) out.push_back(hit);
210 break;
211 }
212 // Stop when close to the boundary.
213 BoundaryPosition pos;
214 const Vec2 bpt = closestPointOnBoundary(poly, np, &pos);
215 if (distance(np, bpt) < boundaryTol) {
216 if (distance(bpt, out.back()) > 1e-9) out.push_back(bpt);
217 break;
218 }
219 out.push_back(np);
220 p = np;
221 }
222 return out;
223}
224
225double polylineChord(const Polyline& pl) {
226 if (pl.size() < 2) return 0.0;
227 return distance(pl.front(), pl.back());
228}
229
230} // namespace
231
232std::vector<SplitCandidate> generateSplitCandidates(const Polygon& poly, int maxCandidates, double minHalfArea) {
233 std::vector<SplitCandidate> out;
234 if (poly.size() < 3) return out;
235 const double polyArea = area(poly);
236 if (polyArea <= 1e-9) return out;
237
238 const double spacing =
239 std::clamp(std::sqrt(polyArea) / 20.0, std::sqrt(polyArea) * 0.01, std::sqrt(polyArea) * 0.2);
240 const int seedCount = std::clamp(int(perimeter(poly) / std::max(1e-6, spacing)), 24, 96);
241 const std::vector<BoundarySample> samples = sampleBoundary(poly, seedCount);
242 const CrossField field = buildCrossField(poly, spacing);
243 const int maxSteps = std::max(64, int(std::ceil(perimeter(poly) / spacing)));
244
245 // Trace from every boundary sample along each field axis that points into the parcel.
246 const Vec2 c = centroid(poly);
247 std::vector<Polyline> traces;
248 for (const BoundarySample& s : samples) {
249 const Vec2 inward = normalize(c - s.p);
250 const double theta = s.tangentAngle;
251 const Vec2 axes[2] = {{std::cos(theta), std::sin(theta)}, {-std::sin(theta), std::cos(theta)}};
252 for (const Vec2& axis : axes) {
253 if (dot(axis, inward) <= 0.05) continue; // axis must point into the parcel
254 Polyline tr = traceStreamline(field, poly, s.p, axis, spacing, maxSteps);
255 if (tr.size() < 4) continue;
256 if (polylineChord(tr) < spacing * 2.0) continue;
257 traces.push_back(std::move(tr));
258 }
259 }
260
261 // Validate + dedupe traces, keep the geometrically best ones.
262 std::vector<SplitCandidate> valid;
263 for (const Polyline& tr : traces) {
264 Polygon a, b;
265 double frac = 0.0;
266 if (!validSplit(poly, tr, minHalfArea, &a, &b, &frac)) continue;
267 SplitCandidate cand;
268 cand.line = tr;
269 cand.areaFrac = frac;
270 valid.push_back(std::move(cand));
271 if (valid.size() >= size_t(maxCandidates) * 3) break;
272 }
273
274 // Straight-chord fallback when tracing gives too few usable splits. Sample a
275 // spread of boundary-separation distances so the generator's metric can choose
276 // among axis-aligned and diagonal candidates.
277 if (valid.size() < size_t(maxCandidates)) {
278 const int k = std::max(32, seedCount);
279 const std::vector<BoundarySample> dense = sampleBoundary(poly, k);
280 std::vector<SplitCandidate> chords;
281 // Arc-separation steps, largest first: near-balanced chords (e.g. top-bottom
282 // connections of a rectangle) are generated before small corner cuts.
283 const int half = k / 2;
284 std::vector<int> steps;
285 for (int s = half; s >= 3; --s) steps.push_back(s);
286 for (const int step : steps) {
287 for (size_t i = 0; i < dense.size(); i += 2) {
288 const size_t j = (i + size_t(step)) % dense.size();
289 if (j == i) continue;
290 Polyline chord{dense[i].p, dense[j].p};
291 Polygon a, b;
292 double frac = 0.0;
293 if (!validSplit(poly, chord, minHalfArea, &a, &b, &frac)) continue;
294 SplitCandidate cand;
295 cand.line = std::move(chord);
296 cand.areaFrac = frac;
297 chords.push_back(std::move(cand));
298 if (chords.size() >= size_t(maxCandidates) * 3) break;
299 }
300 if (chords.size() >= size_t(maxCandidates) * 3) break;
301 }
302 // Append fallbacks (streamlines remain preferred).
303 valid.insert(valid.end(), chords.begin(), chords.end());
304 }
305
306 // Candidate *selection* is left to the quality metric (paper Eq. 2); here we just
307 // keep a diverse set: streamlines first (cross-field), then straight chords, with
308 // near-duplicate lines removed. No geometry-based ranking — that would bias the
309 // metric (e.g. a rectangle's diagonal is perfectly balanced but should lose to a
310 // regular axis-aligned split once regularity is scored).
311 const double eps = std::sqrt(area(poly)) * 1e-3;
312 for (const SplitCandidate& cand : valid) {
313 bool dup = false;
314 for (const SplitCandidate& keep : out) {
315 const double d1 = distance(cand.line.front(), keep.line.front());
316 const double d2 = distance(cand.line.back(), keep.line.back());
317 const double d3 = distance(cand.line.front(), keep.line.back());
318 const double d4 = distance(cand.line.back(), keep.line.front());
319 if ((d1 < eps && d2 < eps) || (d3 < eps && d4 < eps)) {
320 dup = true;
321 break;
322 }
323 }
324 if (dup) continue;
325 out.push_back(cand);
326 if (int(out.size()) >= maxCandidates) break;
327 }
328 return out;
329}
330
331} // namespace eve::procgen::urban
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
const std::string & s
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
Vec3 tangent
Definition CaveMesh.cpp:80
float nx
float ny
glm::vec4 p[6]
float area
Definition Grass.cpp:62
glm::vec3 n
Definition Grass.cpp:63
double r
std::int32_t c
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
Vec3 centroid
float tr
int idx
float f
std::uint32_t seed
Definition PointSet.cpp:807
bool hit
float d
int steps
bool found
float dy
float dx
std::uint32_t count
int spacing
int margin
int iterations
Definition TreeMesh.cpp:311
float step
Definition TreeMesh.cpp:314
std::vector< double > phi
double oy
std::vector< char > inside
double ox
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
bool validSplit(const Polygon &poly, const Polyline &split, double minHalfArea, Polygon *outA, Polygon *outB, double *fracA)
Validity of a candidate split: both halves simple, positive area, inside the original.
std::vector< Vec2 > Polyline
Open polyline (e.g. a streamline candidate or a street centerline).
Definition UrbanTypes.h:57
bool pointInPolygon(const Vec2 &p, const Polygon &poly)
Point-in-polygon test (ray casting; boundary counts as inside).
Vec2 normalize(const Vec2 &a)
Normalize.
Definition UrbanTypes.h:44
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
std::vector< Vec2 > Polygon
Closed polygon ring, stored CCW, without repeating the first point.
Definition UrbanTypes.h:55
double perimeter(const Polygon &poly)
Perimeter length of a closed ring.
std::vector< BoundarySample > sampleBoundary(const Polygon &poly, int count)
Uniformly sample the polygon boundary (approx. count samples, at least 8). Samples are ordered along ...
std::vector< SplitCandidate > generateSplitCandidates(const Polygon &poly, int maxCandidates, double minHalfArea)
Generate candidate streamlines for binary partitioning of poly.
Vec2 closestPointOnBoundary(const Polygon &poly, const Vec2 &p, BoundaryPosition *pos)
Closest point on the polygon boundary; writes edge index + t in [0,1].
const EditorValue * field(const EditorValue &value, const char *name)
A boundary sample used for field constraints / candidate seeds.
Definition UrbanTypes.h:158
Candidate splitting lines for a parcel (paper Section 4.1). Each candidate is a polyline from one bou...
Minimal 2D vector used by the urban layout algorithms (paper coordinates).
Definition UrbanTypes.h:15