载入中...
搜索中...
未找到
MeshModifierGraph.cpp
浏览该文件的文档.
5
6#include <algorithm>
7#include <array>
8#include <chrono>
9#include <cmath>
10#include <cstddef>
11#include <cstdint>
12#include <functional>
13#include <limits>
14#include <unordered_set>
15
16namespace eve::procgen {
17namespace {
18
19struct ParamSpec {
20 const char* key;
21 const char* kind;
22 const char* defaultValue;
23};
24
25struct OperationSpec {
26 const char* id;
27 int inputs;
29 std::vector<ParamSpec> params;
30};
31
32const std::vector<OperationSpec>& operationSpecs() {
33 static const std::vector<OperationSpec> specs = [] {
34 std::vector<OperationSpec> values = {
35 {"mesh.input", 0, false, {}},
36 {"mesh.output", 1, false, {}},
37 {"mesh.splineTube",
38 0,
39 false,
40 {{"radius", "float", "0.5"},
41 {"pathSegments", "int", "32"},
42 {"radialSegments", "int", "12"},
43 {"roll", "float", "0"},
44 {"cap", "int", "1"}}},
45 {"mesh.splineRibbon",
46 0,
47 false,
48 {{"width", "float", "2"},
49 {"thickness", "float", "0"},
50 {"pathSegments", "int", "32"},
51 {"roll", "float", "0"},
52 {"cap", "int", "1"}}},
53 {"mesh.splineExtrude",
54 0,
55 false,
56 {{"pathSegments", "int", "32"}, {"roll", "float", "0"}, {"cap", "int", "1"}}},
57 {"deform.transform",
58 1,
59 true,
60 {{"x", "float", "0"},
61 {"y", "float", "0"},
62 {"z", "float", "0"},
63 {"pitch", "float", "0"},
64 {"yaw", "float", "0"},
65 {"roll", "float", "0"},
66 {"scaleX", "float", "1"},
67 {"scaleY", "float", "1"},
68 {"scaleZ", "float", "1"}}},
69 {"deform.bend", 1, true, {{"axis", "string", "y"}, {"angle", "float", "0"}, {"extent", "float", "1"}}},
70 {"deform.twist", 1, true, {{"axis", "string", "y"}, {"angle", "float", "0"}, {"extent", "float", "1"}}},
71 {"deform.noise",
72 1,
73 true,
74 {{"amplitude", "float", "0.1"}, {"frequency", "float", "1"}, {"seed", "int", "1"}}},
75 {"deform.soundReact",
76 1,
77 true,
78 {{"level", "float", "0"},
79 {"threshold", "float", "0"},
80 {"strength", "float", "1"},
81 {"frequency", "float", "1"},
82 {"phase", "float", "0"},
83 {"axis", "string", "y"}}},
84 {"deform.radial",
85 1,
86 true,
87 {{"x", "float", "0"},
88 {"y", "float", "0"},
89 {"z", "float", "0"},
90 {"radius", "float", "1"},
91 {"strength", "float", "0.1"},
92 {"falloff", "float", "1"}}},
93 {"deform.effector",
94 1,
95 true,
96 {{"pointCount", "int", "1"}, {"density", "float", "1"}, {"multiplier", "float", "1"},
97 {"p0x", "float", "0"}, {"p0y", "float", "0"}, {"p0z", "float", "0"},
98 {"p0dx", "float", "0"}, {"p0dy", "float", "1"}, {"p0dz", "float", "0"},
99 {"p0radius", "float", "1"}, {"p0weight", "float", "1"}, {"p1x", "float", "0"},
100 {"p1y", "float", "0"}, {"p1z", "float", "0"}, {"p1dx", "float", "0"},
101 {"p1dy", "float", "1"}, {"p1dz", "float", "0"}, {"p1radius", "float", "1"},
102 {"p1weight", "float", "1"}, {"p2x", "float", "0"}, {"p2y", "float", "0"},
103 {"p2z", "float", "0"}, {"p2dx", "float", "0"}, {"p2dy", "float", "1"},
104 {"p2dz", "float", "0"}, {"p2radius", "float", "1"}, {"p2weight", "float", "1"},
105 {"p3x", "float", "0"}, {"p3y", "float", "0"}, {"p3z", "float", "0"},
106 {"p3dx", "float", "0"}, {"p3dy", "float", "1"}, {"p3dz", "float", "0"},
107 {"p3radius", "float", "1"}, {"p3weight", "float", "1"}}},
108 {"deform.angularBend",
109 1,
110 false,
111 {{"angle", "float", "0"}, {"direction", "float", "0"}, {"axis", "string", "y"}}},
112 {"deform.spherify",
113 1,
114 true,
115 {{"x", "float", "0"},
116 {"y", "float", "0"},
117 {"z", "float", "0"},
118 {"radius", "float", "1"},
119 {"weight", "float", "1"}}},
120 {"deform.ffd", 1, false, {{"p000x", "float", "0"}, {"p000y", "float", "0"}, {"p000z", "float", "0"},
121 {"p001x", "float", "0"}, {"p001y", "float", "0"}, {"p001z", "float", "0"},
122 {"p010x", "float", "0"}, {"p010y", "float", "0"}, {"p010z", "float", "0"},
123 {"p011x", "float", "0"}, {"p011y", "float", "0"}, {"p011z", "float", "0"},
124 {"p100x", "float", "0"}, {"p100y", "float", "0"}, {"p100z", "float", "0"},
125 {"p101x", "float", "0"}, {"p101y", "float", "0"}, {"p101z", "float", "0"},
126 {"p110x", "float", "0"}, {"p110y", "float", "0"}, {"p110z", "float", "0"},
127 {"p111x", "float", "0"}, {"p111y", "float", "0"}, {"p111z", "float", "0"},
128 {"weight", "float", "1"}}},
129 {"deform.morph", 2, false, {{"weight", "float", "0.5"}}},
130 {"deform.meshFit",
131 2,
132 false,
133 {{"directionX", "float", "0"},
134 {"directionY", "float", "-1"},
135 {"directionZ", "float", "0"},
136 {"maxDistance", "float", "10"},
137 {"surfaceOffset", "float", "0"},
138 {"bidirectional", "int", "0"}}},
139 {"deform.meshAdhere",
140 2,
141 false,
142 {{"strength", "float", "1"},
143 {"edgeRadius", "float", "0.5"},
144 {"materialRadius", "float", "0.5"},
145 {"normalsBlend", "float", "1"},
146 {"materialBlend", "float", "1"},
147 {"surfaceOffset", "float", "0"},
148 {"maxQueryDistance", "float", "0"},
149 {"softSnapPositions", "int", "1"},
150 {"falloff", "string", "smooth"}}},
151 {"deform.spline",
152 1,
153 false,
154 {{"axis", "string", "y"},
155 {"c0x", "float", "0"},
156 {"c0y", "float", "0"},
157 {"c0z", "float", "0"},
158 {"c1x", "float", "0"},
159 {"c1y", "float", "0"},
160 {"c1z", "float", "0"},
161 {"c2x", "float", "0"},
162 {"c2y", "float", "0"},
163 {"c2z", "float", "0"},
164 {"c3x", "float", "0"},
165 {"c3y", "float", "0"},
166 {"c3z", "float", "0"},
167 {"weight", "float", "1"}}},
168 {"deform.splinePath",
169 1,
170 false,
171 {{"axis", "string", "y"}, {"weight", "float", "1"}, {"roll", "float", "0"}, {"scale", "float", "1"}}},
172 {"deform.smooth", 1, false, {{"strength", "float", "0.5"}, {"iterations", "int", "1"}}},
173 {"mesh.subdivide", 1, false, {{"levels", "int", "1"}}},
174 {"mesh.cutPlane",
175 1,
176 false,
177 {{"normalX", "float", "0"},
178 {"normalY", "float", "1"},
179 {"normalZ", "float", "0"},
180 {"distance", "float", "0"},
181 {"keepPositive", "int", "1"},
182 {"cap", "int", "0"}}},
183 {"mesh.append", 2, false, {}},
184 {"mesh.boolean", 2, false, {{"operation", "string", "difference"}}},
185 {"mesh.projectUv", 1, false, {{"mode", "string", "box"}, {"scale", "float", "1"},
186 {"offsetU", "float", "0"}, {"offsetV", "float", "0"}}},
187 {"mesh.weld", 1, false, {{"tolerance", "float", "0.0001"}}},
188 };
189 for (auto& spec : values) {
190 if (!spec.perVertex) continue;
191 spec.params.insert(spec.params.end(), {{"maskX", "float", "0"},
192 {"maskY", "float", "0"},
193 {"maskZ", "float", "0"},
194 {"maskRadius", "float", "0"},
195 {"maskFalloff", "float", "1"},
196 {"maskInvert", "int", "0"}});
197 }
198 return values;
199 }();
200 return specs;
201}
202
203const OperationSpec* findSpec(std::string_view operation) {
204 const auto& specs = operationSpecs();
205 const auto found =
206 std::find_if(specs.begin(), specs.end(), [&](const OperationSpec& spec) { return operation == spec.id; });
207 return found == specs.end() ? nullptr : &*found;
208}
209
210const ParamSpec* findParam(const OperationSpec& spec, std::string_view key) {
211 const auto found =
212 std::find_if(spec.params.begin(), spec.params.end(), [&](const ParamSpec& param) { return key == param.key; });
213 return found == spec.params.end() ? nullptr : &*found;
214}
215
216float parameter(const auto& node, std::string_view key, float fallback) {
217 const auto found = node.floats.find(std::string(key));
218 return found == node.floats.end() ? fallback : found->second;
219}
220
221int intParameter(const auto& node, std::string_view key, int fallback) {
222 const auto found = node.ints.find(std::string(key));
223 return found == node.ints.end() ? fallback : found->second;
224}
225
226std::string stringParameter(const auto& node, std::string_view key, std::string fallback) {
227 const auto found = node.strings.find(std::string(key));
228 return found == node.strings.end() ? std::move(fallback) : found->second;
229}
230
231struct Vec3 {
232 float x = 0.f, y = 0.f, z = 0.f;
233};
234
235Vec3 rotateX(Vec3 value, float radians) {
236 const float c = std::cos(radians), s = std::sin(radians);
237 return {value.x, c * value.y - s * value.z, s * value.y + c * value.z};
238}
239
240Vec3 rotateY(Vec3 value, float radians) {
241 const float c = std::cos(radians), s = std::sin(radians);
242 return {c * value.x + s * value.z, value.y, -s * value.x + c * value.z};
243}
244
245Vec3 rotateZ(Vec3 value, float radians) {
246 const float c = std::cos(radians), s = std::sin(radians);
247 return {c * value.x - s * value.y, s * value.x + c * value.y, value.z};
248}
249
250float coherentNoise(Vec3 value, int seed) {
251 const float phase = static_cast<float>(seed);
252 return std::sin(value.x * 1.13f + value.y * 0.71f + value.z * 0.47f + phase * 0.123f) * 0.5f +
253 std::sin(value.x * 0.37f - value.y * 1.41f + value.z * 0.89f + phase * 0.071f) * 0.3f +
254 std::sin(-value.x * 0.83f + value.y * 0.29f + value.z * 1.67f + phase * 0.037f) * 0.2f;
255}
256
257void applyVertexOperation(const auto& node, Vec3& position, Vec3& normal) {
258 constexpr float radiansPerDegree = 0.01745329251994329577f;
259 if (node.operation == "deform.transform") {
260 const float sx = parameter(node, "scaleX", 1.f);
261 const float sy = parameter(node, "scaleY", 1.f);
262 const float sz = parameter(node, "scaleZ", 1.f);
263 position = {position.x * sx, position.y * sy, position.z * sz};
264 normal = {normal.x / sx, normal.y / sy, normal.z / sz};
265 position = rotateZ(rotateY(rotateX(position, parameter(node, "pitch", 0.f) * radiansPerDegree),
266 parameter(node, "yaw", 0.f) * radiansPerDegree),
267 parameter(node, "roll", 0.f) * radiansPerDegree);
268 normal = rotateZ(rotateY(rotateX(normal, parameter(node, "pitch", 0.f) * radiansPerDegree),
269 parameter(node, "yaw", 0.f) * radiansPerDegree),
270 parameter(node, "roll", 0.f) * radiansPerDegree);
271 position.x += parameter(node, "x", 0.f);
272 position.y += parameter(node, "y", 0.f);
273 position.z += parameter(node, "z", 0.f);
274 } else if (node.operation == "deform.twist" || node.operation == "deform.bend") {
275 const std::string axis = stringParameter(node, "axis", "y");
276 const float extent = std::max(std::abs(parameter(node, "extent", 1.f)), 1e-6f);
277 const float along = axis == "x" ? position.x : axis == "z" ? position.z : position.y;
278 const float angle = parameter(node, "angle", 0.f) * radiansPerDegree * along / extent;
279 if (node.operation == "deform.twist") {
280 if (axis == "x") {
281 position = rotateX(position, angle);
282 normal = rotateX(normal, angle);
283 } else if (axis == "z") {
284 position = rotateZ(position, angle);
285 normal = rotateZ(normal, angle);
286 } else {
287 position = rotateY(position, angle);
288 normal = rotateY(normal, angle);
289 }
290 } else {
291 if (axis == "x") {
292 position = rotateZ(position, angle);
293 normal = rotateZ(normal, angle);
294 } else if (axis == "z") {
295 position = rotateX(position, -angle);
296 normal = rotateX(normal, -angle);
297 } else {
298 position = rotateZ(position, -angle);
299 normal = rotateZ(normal, -angle);
300 }
301 }
302 } else if (node.operation == "deform.noise") {
303 const float frequency = parameter(node, "frequency", 1.f);
304 const Vec3 sample{position.x * frequency, position.y * frequency, position.z * frequency};
305 const int seed = intParameter(node, "seed", 1);
306 const float amplitude = parameter(node, "amplitude", 0.1f);
307 position.x += coherentNoise(sample, seed) * amplitude;
308 position.y += coherentNoise({sample.y, sample.z, sample.x}, seed + 1013) * amplitude;
309 position.z += coherentNoise({sample.z, sample.x, sample.y}, seed + 2029) * amplitude;
310 } else if (node.operation == "deform.soundReact") {
311 const std::string axis = stringParameter(node, "axis", "y");
312 const float coordinate = axis == "x" ? position.x : (axis == "z" ? position.z : position.y);
313 const float level = parameter(node, "level", 0.f);
314 const float threshold = parameter(node, "threshold", 0.f);
315 const float response = std::max(level - threshold, 0.f) * parameter(node, "strength", 1.f);
316 const float wave = 0.5f + 0.5f * std::sin(coordinate * parameter(node, "frequency", 1.f) * 6.283185307f +
317 parameter(node, "phase", 0.f));
318 Vec3 radial = position;
319 if (axis == "x") radial.x = 0.f;
320 if (axis == "y") radial.y = 0.f;
321 if (axis == "z") radial.z = 0.f;
322 const float radialLength = std::sqrt(radial.x * radial.x + radial.y * radial.y + radial.z * radial.z);
323 if (radialLength > 1e-7f) {
324 const float amount = response * wave / radialLength;
325 position.x += radial.x * amount;
326 position.y += radial.y * amount;
327 position.z += radial.z * amount;
328 }
329 } else if (node.operation == "deform.radial") {
330 const Vec3 center{parameter(node, "x", 0.f), parameter(node, "y", 0.f), parameter(node, "z", 0.f)};
331 Vec3 delta{position.x - center.x, position.y - center.y, position.z - center.z};
332 const float distance = std::sqrt(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
333 const float radius = std::max(parameter(node, "radius", 1.f), 1e-6f);
334 if (distance < radius && distance > 1e-7f) {
335 const float falloff = std::max(parameter(node, "falloff", 1.f), 0.01f);
336 const float amount = parameter(node, "strength", 0.1f) * std::pow(1.f - distance / radius, falloff);
337 position.x += delta.x / distance * amount;
338 position.y += delta.y / distance * amount;
339 position.z += delta.z / distance * amount;
340 }
341 } else if (node.operation == "deform.effector") {
342 const int pointCount = std::clamp(intParameter(node, "pointCount", 1), 1, 4);
343 const float density = std::max(parameter(node, "density", 1.f), 0.01f);
344 const float multiplier = parameter(node, "multiplier", 1.f);
345 const Vec3 source = position;
346 for (int point = 0; point < pointCount; ++point) {
347 const std::string prefix = "p" + std::to_string(point);
348 const Vec3 center{parameter(node, prefix + "x", 0.f), parameter(node, prefix + "y", 0.f),
349 parameter(node, prefix + "z", 0.f)};
350 const Vec3 offset{parameter(node, prefix + "dx", 0.f), parameter(node, prefix + "dy", 1.f),
351 parameter(node, prefix + "dz", 0.f)};
352 const float dx = source.x - center.x;
353 const float dy = source.y - center.y;
354 const float dz = source.z - center.z;
355 const float distance = std::sqrt(dx * dx + dy * dy + dz * dz);
356 const float radius = parameter(node, prefix + "radius", 1.f);
357 if (distance >= radius) continue;
358 const float influence =
359 std::pow(1.f - distance / radius, density) * parameter(node, prefix + "weight", 1.f) * multiplier;
360 position.x += offset.x * influence;
361 position.y += offset.y * influence;
362 position.z += offset.z * influence;
363 }
364 } else if (node.operation == "deform.spherify") {
365 const Vec3 center{parameter(node, "x", 0.f), parameter(node, "y", 0.f), parameter(node, "z", 0.f)};
366 Vec3 delta{position.x - center.x, position.y - center.y, position.z - center.z};
367 const float length = std::sqrt(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
368 if (length > 1e-7f) {
369 const float radius = std::max(parameter(node, "radius", 1.f), 0.f);
370 const float weight = std::clamp(parameter(node, "weight", 1.f), 0.f, 1.f);
371 const Vec3 target{center.x + delta.x * radius / length, center.y + delta.y * radius / length,
372 center.z + delta.z * radius / length};
373 position = {std::lerp(position.x, target.x, weight), std::lerp(position.y, target.y, weight),
374 std::lerp(position.z, target.z, weight)};
375 normal = {delta.x / length, delta.y / length, delta.z / length};
376 }
377 }
378 const float length = std::sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
379 if (length > 1e-7f) normal = {normal.x / length, normal.y / length, normal.z / length};
380}
381
382void applyMaskedVertexOperation(const auto& node, Vec3& position, Vec3& normal) {
383 const Vec3 originalPosition = position;
384 const Vec3 originalNormal = normal;
385 applyVertexOperation(node, position, normal);
386 const float radius = parameter(node, "maskRadius", 0.f);
387 if (radius <= 0.f) return;
388 const float dx = originalPosition.x - parameter(node, "maskX", 0.f);
389 const float dy = originalPosition.y - parameter(node, "maskY", 0.f);
390 const float dz = originalPosition.z - parameter(node, "maskZ", 0.f);
391 const float distance = std::sqrt(dx * dx + dy * dy + dz * dz);
392 float weight =
393 std::pow(std::clamp(1.f - distance / radius, 0.f, 1.f), std::max(parameter(node, "maskFalloff", 1.f), 0.01f));
394 if (intParameter(node, "maskInvert", 0) != 0) weight = 1.f - weight;
395 position = {std::lerp(originalPosition.x, position.x, weight), std::lerp(originalPosition.y, position.y, weight),
396 std::lerp(originalPosition.z, position.z, weight)};
397 normal = {std::lerp(originalNormal.x, normal.x, weight), std::lerp(originalNormal.y, normal.y, weight),
398 std::lerp(originalNormal.z, normal.z, weight)};
399 const float normalLength = std::sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
400 if (normalLength > 1e-7f) normal = {normal.x / normalLength, normal.y / normalLength, normal.z / normalLength};
401}
402
403Result<void> validateMesh(const MeshBuild& mesh, std::string_view path) {
404 if (mesh.empty()) return Result<void>::failure(
405 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh input is empty", std::string(path), {}, "procgen.meshModifierGraph"));
406 if (mesh.positions().size() % 3u != 0u || mesh.normals().size() != mesh.positions().size() ||
407 mesh.uvs().size() != (mesh.positions().size() / 3u) * 2u || mesh.indices().size() % 3u != 0u)
409 Diagnostic::error(DiagnosticCode::InvariantViolation, "mesh streams have incompatible lengths", std::string(path), {}, "procgen.meshModifierGraph"));
410 for (const auto index : mesh.indices())
411 if (index >= static_cast<std::uint32_t>(mesh.getVertexCount()))
412 return Result<void>::failure(
413 Diagnostic::error(DiagnosticCode::InvariantViolation, "mesh index is outside the vertex stream", std::string(path), {}, "procgen.meshModifierGraph"));
414 return Result<void>::success();
415}
416
417void recalculateNormals(MeshBuild& mesh) {
418 auto& normals = mesh.normals();
419 std::fill(normals.begin(), normals.end(), 0.f);
420 const auto& positions = mesh.positions();
421 const auto& indices = mesh.indices();
422 for (std::size_t i = 0; i + 2 < indices.size(); i += 3) {
423 const std::size_t a = std::size_t(indices[i]) * 3u;
424 const std::size_t b = std::size_t(indices[i + 1]) * 3u;
425 const std::size_t c = std::size_t(indices[i + 2]) * 3u;
426 const Vec3 ab{positions[b] - positions[a], positions[b + 1] - positions[a + 1],
427 positions[b + 2] - positions[a + 2]};
428 const Vec3 ac{positions[c] - positions[a], positions[c + 1] - positions[a + 1],
429 positions[c + 2] - positions[a + 2]};
430 const Vec3 cross{ab.y * ac.z - ab.z * ac.y, ab.z * ac.x - ab.x * ac.z, ab.x * ac.y - ab.y * ac.x};
431 for (const auto vertex : {a, b, c}) {
432 normals[vertex] += cross.x;
433 normals[vertex + 1] += cross.y;
434 normals[vertex + 2] += cross.z;
435 }
436 }
437 for (std::size_t i = 0; i + 2 < normals.size(); i += 3) {
438 const float length =
439 std::sqrt(normals[i] * normals[i] + normals[i + 1] * normals[i + 1] + normals[i + 2] * normals[i + 2]);
440 if (length > 1e-7f) {
441 normals[i] /= length;
442 normals[i + 1] /= length;
443 normals[i + 2] /= length;
444 }
445 }
446}
447
448Result<MeshBuild> smoothMesh(const MeshBuild& input, float strength, int iterations) {
449 MeshBuild output = input;
450 const int count = output.getVertexCount();
451 std::vector<std::unordered_set<std::uint32_t>> neighbors(static_cast<std::size_t>(count));
452 const auto& indices = output.indices();
453 for (std::size_t i = 0; i + 2 < indices.size(); i += 3) {
454 const std::array<std::uint32_t, 3> tri{indices[i], indices[i + 1], indices[i + 2]};
455 for (int corner = 0; corner < 3; ++corner) {
456 neighbors[tri[corner]].insert(tri[(corner + 1) % 3]);
457 neighbors[tri[corner]].insert(tri[(corner + 2) % 3]);
458 }
459 }
460 strength = std::clamp(strength, 0.f, 1.f);
461 iterations = std::clamp(iterations, 1, 64);
462 std::vector<float> next = output.positions();
463 for (int iteration = 0; iteration < iterations; ++iteration) {
464 const auto current = output.positions();
465 for (int vertex = 0; vertex < count; ++vertex) {
466 const auto& adjacent = neighbors[static_cast<std::size_t>(vertex)];
467 if (adjacent.empty()) continue;
468 Vec3 average;
469 for (const auto neighbor : adjacent) {
470 average.x += current[std::size_t(neighbor) * 3u];
471 average.y += current[std::size_t(neighbor) * 3u + 1u];
472 average.z += current[std::size_t(neighbor) * 3u + 2u];
473 }
474 const float divisor = float(adjacent.size());
475 const std::size_t base = std::size_t(vertex) * 3u;
476 next[base] = std::lerp(current[base], average.x / divisor, strength);
477 next[base + 1] = std::lerp(current[base + 1], average.y / divisor, strength);
478 next[base + 2] = std::lerp(current[base + 2], average.z / divisor, strength);
479 }
480 output.positions() = next;
481 }
482 // Geometry normals + 1-ring average so post-fusion relax does not reintroduce neck creases.
484 return Result<MeshBuild>::success(std::move(output));
485}
486
487void copyMetadata(const MeshBuild& input, MeshBuild& output) {
488 for (const auto& [key, value] : input.metadata()) output.setMeta(key, value);
489}
490
491Result<MeshBuild> angularBendMesh(const MeshBuild& input, const auto& node) {
492 MeshBuild output = input;
493 auto& positions = output.positions();
494 if (positions.empty()) return Result<MeshBuild>::success(std::move(output));
495 const std::string axis = stringParameter(node, "axis", "y");
496 const int component = axis == "x" ? 0 : axis == "z" ? 2 : 1;
497 float minimum = positions[static_cast<std::size_t>(component)];
498 float maximum = minimum;
499 for (std::size_t i = static_cast<std::size_t>(component); i < positions.size(); i += 3u) {
500 minimum = std::min(minimum, positions[i]);
501 maximum = std::max(maximum, positions[i]);
502 }
503 const float extent = std::max(maximum - minimum, 1e-6f);
504 const float direction = parameter(node, "direction", 0.f) * 0.01745329251994329577f;
505 const Vec3 bendAxis{std::sin(direction), 0.f, std::cos(direction)};
506 const float angle = parameter(node, "angle", 0.f) * 0.01745329251994329577f;
507 auto& normals = output.normals();
508 for (int vertex = 0; vertex < output.getVertexCount(); ++vertex) {
509 const std::size_t base = static_cast<std::size_t>(vertex) * 3u;
510 const float t = (positions[base + static_cast<std::size_t>(component)] - minimum) / extent;
511 const float theta = angle * t;
512 const float c = std::cos(theta), s = std::sin(theta);
513 auto rotate = [&](Vec3 value) {
514 const float dot = value.x * bendAxis.x + value.y * bendAxis.y + value.z * bendAxis.z;
515 const Vec3 cross{bendAxis.y * value.z - bendAxis.z * value.y, bendAxis.z * value.x - bendAxis.x * value.z,
516 bendAxis.x * value.y - bendAxis.y * value.x};
517 return Vec3{value.x * c + cross.x * s + bendAxis.x * dot * (1.f - c),
518 value.y * c + cross.y * s + bendAxis.y * dot * (1.f - c),
519 value.z * c + cross.z * s + bendAxis.z * dot * (1.f - c)};
520 };
521 const Vec3 position = rotate({positions[base], positions[base + 1], positions[base + 2]});
522 const Vec3 normal = rotate({normals[base], normals[base + 1], normals[base + 2]});
523 positions[base] = position.x;
524 positions[base + 1] = position.y;
525 positions[base + 2] = position.z;
526 normals[base] = normal.x;
527 normals[base + 1] = normal.y;
528 normals[base + 2] = normal.z;
529 }
530 return Result<MeshBuild>::success(std::move(output));
531}
532
533Result<MeshBuild> splineMesh(const MeshBuild& input, const auto& node) {
534 MeshBuild output = input;
535 auto& positions = output.positions();
536 if (positions.empty()) return Result<MeshBuild>::success(std::move(output));
537 const std::string axis = stringParameter(node, "axis", "y");
538 const int component = axis == "x" ? 0 : axis == "z" ? 2 : 1;
539 float minimum = positions[static_cast<std::size_t>(component)];
540 float maximum = minimum;
541 for (std::size_t i = static_cast<std::size_t>(component); i < positions.size(); i += 3u) {
542 minimum = std::min(minimum, positions[i]);
543 maximum = std::max(maximum, positions[i]);
544 }
545 const float extent = std::max(maximum - minimum, 1e-6f);
546 std::array<Vec3, 4> controls;
547 for (int i = 0; i < 4; ++i) {
548 const std::string prefix = "c" + std::to_string(i);
549 controls[static_cast<std::size_t>(i)] = {parameter(node, prefix + "x", 0.f), parameter(node, prefix + "y", 0.f),
550 parameter(node, prefix + "z", 0.f)};
551 }
552 const float weight = std::clamp(parameter(node, "weight", 1.f), 0.f, 1.f);
553 auto bezier = [&](float t) {
554 const float u = 1.f - t;
555 const std::array<float, 4> b{u * u * u, 3.f * u * u * t, 3.f * u * t * t, t * t * t};
556 Vec3 result;
557 for (int i = 0; i < 4; ++i) {
558 result.x += controls[static_cast<std::size_t>(i)].x * b[static_cast<std::size_t>(i)];
559 result.y += controls[static_cast<std::size_t>(i)].y * b[static_cast<std::size_t>(i)];
560 result.z += controls[static_cast<std::size_t>(i)].z * b[static_cast<std::size_t>(i)];
561 }
562 return result;
563 };
564 for (std::size_t base = 0; base < positions.size(); base += 3u) {
565 Vec3 source{positions[base], positions[base + 1], positions[base + 2]};
566 const float t = std::clamp((component == 0 ? source.x
567 : component == 2 ? source.z
568 : source.y) -
569 minimum,
570 0.f, extent) /
571 extent;
572 const Vec3 offset = bezier(t);
573 positions[base] = source.x + offset.x * weight;
574 positions[base + 1] = source.y + offset.y * weight;
575 positions[base + 2] = source.z + offset.z * weight;
576 }
577 recalculateNormals(output);
578 return Result<MeshBuild>::success(std::move(output));
579}
580
581Result<MeshBuild> splinePathMesh(const MeshBuild& input, const auto& node) {
582 MeshBuild output = input;
583 auto& positions = output.positions();
584 if (positions.empty()) return Result<MeshBuild>::success(std::move(output));
585 const std::string axis = stringParameter(node, "axis", "y");
586 const int component = axis == "x" ? 0 : axis == "z" ? 2 : 1;
587 float minimum = positions[static_cast<std::size_t>(component)];
588 float maximum = minimum;
589 for (std::size_t i = static_cast<std::size_t>(component); i < positions.size(); i += 3u) {
590 minimum = std::min(minimum, positions[i]);
591 maximum = std::max(maximum, positions[i]);
592 }
593 const float extent = std::max(maximum - minimum, 1e-6f);
594 const float weight = std::clamp(parameter(node, "weight", 1.f), 0.f, 1.f);
595 const float scale = parameter(node, "scale", 1.f);
596 const float roll = parameter(node, "roll", 0.f) * 0.01745329251994329577f;
597 for (std::size_t base = 0; base < positions.size(); base += 3u) {
598 const Vec3 source{positions[base], positions[base + 1], positions[base + 2]};
599 const float along = component == 0 ? source.x : component == 2 ? source.z : source.y;
600 auto sampled = node.splinePath.evaluateResult((along - minimum) / extent);
601 if (!sampled.ok()) return Result<MeshBuild>::failure(sampled.status());
602 const auto& sample = sampled.value();
603 Vec3 tangent{sample.tangentX, sample.tangentY, sample.tangentZ};
604 Vec3 side, up;
605 if (tangent.z < -0.999999f) {
606 side = {0.f, -1.f, 0.f};
607 up = {-1.f, 0.f, 0.f};
608 } else {
609 const float a = 1.f / (1.f + tangent.z);
610 const float b = -tangent.x * tangent.y * a;
611 side = {1.f - tangent.x * tangent.x * a, b, -tangent.x};
612 up = {b, 1.f - tangent.y * tangent.y * a, -tangent.y};
613 }
614 const float c = std::cos(roll), s = std::sin(roll);
615 const Vec3 rolledSide{side.x * c + up.x * s, side.y * c + up.y * s, side.z * c + up.z * s};
616 const Vec3 rolledUp{up.x * c - side.x * s, up.y * c - side.y * s, up.z * c - side.z * s};
617 const float localA = component == 0 ? source.y : source.x;
618 const float localB = component == 2 ? source.y : source.z;
619 const Vec3 target{sample.x + (rolledSide.x * localA + rolledUp.x * localB) * scale,
620 sample.y + (rolledSide.y * localA + rolledUp.y * localB) * scale,
621 sample.z + (rolledSide.z * localA + rolledUp.z * localB) * scale};
622 positions[base] = std::lerp(source.x, target.x, weight);
623 positions[base + 1] = std::lerp(source.y, target.y, weight);
624 positions[base + 2] = std::lerp(source.z, target.z, weight);
625 }
626 recalculateNormals(output);
627 return Result<MeshBuild>::success(std::move(output));
628}
629
630Result<MeshBuild> splineTubeMesh(const auto& node) {
631 if (node.splinePath.chunkCount() > 1) {
632 MeshBuild output;
633 for (int chunk = 0; chunk < node.splinePath.chunkCount(); ++chunk) {
634 auto path = node.splinePath.chunkPathResult(chunk);
635 if (!path.ok()) return Result<MeshBuild>::failure(path.status());
636 auto chunkNode = node;
637 chunkNode.splinePath = std::move(path).takeValue();
638 auto mesh = splineTubeMesh(chunkNode);
639 if (!mesh.ok()) return mesh;
640 if (!output.appendTransformed(&mesh.value(), 0.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f))
642 "failed to compose spline tube chunks", {}, {},
643 "procgen.meshModifierGraph"));
644 }
645 output.setMeta("generator", "mesh.splineTube");
646 output.setMeta("chunks", std::to_string(node.splinePath.chunkCount()));
647 return Result<MeshBuild>::success(std::move(output));
648 }
649 const float radius = parameter(node, "radius", 0.5f);
650 const int pathSegments = intParameter(node, "pathSegments", 32);
651 const int radialSegments = intParameter(node, "radialSegments", 12);
652 const float roll = parameter(node, "roll", 0.f);
653 const bool capped = intParameter(node, "cap", 1) != 0 && !node.splinePath.isClosed();
654 const int ringStride = radialSegments + 1;
655 const int ringCount = pathSegments + 1;
656 MeshBuild output;
657 output.reserve(ringCount * ringStride + (capped ? 2 * (ringStride + 1) : 0),
658 pathSegments * radialSegments * 6 + (capped ? radialSegments * 6 : 0));
659
660 constexpr float tau = 6.28318530717958647692f;
661 auto sampledFrames = node.splinePath.sampleFramesResult(pathSegments, true, roll, 24);
662 if (!sampledFrames.ok()) return Result<MeshBuild>::failure(sampledFrames.status());
663 const auto& frames = sampledFrames.value();
664 for (int ring = 0; ring < ringCount; ++ring) {
665 const float u = static_cast<float>(ring) / static_cast<float>(pathSegments);
666 const auto& frame = frames[static_cast<std::size_t>(ring)];
667 const auto& sample = frame.sample;
668 const Vec3 rolledSide{frame.sideX, frame.sideY, frame.sideZ};
669 const Vec3 rolledUp{frame.upX, frame.upY, frame.upZ};
670 for (int radial = 0; radial <= radialSegments; ++radial) {
671 const float v = static_cast<float>(radial) / static_cast<float>(radialSegments);
672 const float angle = v * tau;
673 const float c = std::cos(angle), s = std::sin(angle);
674 const Vec3 offset{rolledSide.x * c * sample.scaleX + rolledUp.x * s * sample.scaleY,
675 rolledSide.y * c * sample.scaleX + rolledUp.y * s * sample.scaleY,
676 rolledSide.z * c * sample.scaleX + rolledUp.z * s * sample.scaleY};
677 const float inverseX = 1.f / sample.scaleX, inverseY = 1.f / sample.scaleY;
678 Vec3 normal{rolledSide.x * c * inverseX + rolledUp.x * s * inverseY,
679 rolledSide.y * c * inverseX + rolledUp.y * s * inverseY,
680 rolledSide.z * c * inverseX + rolledUp.z * s * inverseY};
681 const float normalLength = std::sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
682 normal = {normal.x / normalLength, normal.y / normalLength, normal.z / normalLength};
683 output.addVertex(sample.x + offset.x * radius, sample.y + offset.y * radius, sample.z + offset.z * radius,
684 normal.x, normal.y, normal.z, u, v);
685 }
686 }
687
688 output.setActiveGroup("tube");
689 for (int ring = 0; ring < pathSegments; ++ring) {
690 for (int radial = 0; radial < radialSegments; ++radial) {
691 const auto a = static_cast<std::uint32_t>(ring * ringStride + radial);
692 const auto b = a + static_cast<std::uint32_t>(ringStride);
693 output.addTriangle(a, a + 1u, b);
694 output.addTriangle(a + 1u, b + 1u, b);
695 }
696 }
697 if (capped) {
698 output.setActiveGroup("caps");
699 auto startSample = node.splinePath.evaluateResult(0.f);
700 auto endSample = node.splinePath.evaluateResult(1.f);
701 if (!startSample.ok()) return Result<MeshBuild>::failure(startSample.status());
702 if (!endSample.ok()) return Result<MeshBuild>::failure(endSample.status());
703 const Vec3 startNormal{-startSample.value().tangentX, -startSample.value().tangentY,
704 -startSample.value().tangentZ};
705 const Vec3 endNormal{endSample.value().tangentX, endSample.value().tangentY, endSample.value().tangentZ};
706 const auto startCenter = static_cast<std::uint32_t>(output.getVertexCount());
707 output.addVertex(startSample.value().x, startSample.value().y, startSample.value().z, startNormal.x,
708 startNormal.y, startNormal.z, 0.5f, 0.5f);
709 const auto startRim = static_cast<std::uint32_t>(output.getVertexCount());
710 for (int radial = 0; radial <= radialSegments; ++radial) {
711 const float angle = static_cast<float>(radial) / static_cast<float>(radialSegments) * tau;
712 output.addVertex(output.getPositionX(radial), output.getPositionY(radial), output.getPositionZ(radial),
713 startNormal.x, startNormal.y, startNormal.z, 0.5f + std::cos(angle) * 0.5f,
714 0.5f + std::sin(angle) * 0.5f);
715 }
716 const auto endCenter = static_cast<std::uint32_t>(output.getVertexCount());
717 output.addVertex(endSample.value().x, endSample.value().y, endSample.value().z, endSample.value().tangentX,
718 endSample.value().tangentY, endSample.value().tangentZ, 0.5f, 0.5f);
719 const auto lastRing = static_cast<std::uint32_t>(pathSegments * ringStride);
720 const auto endRim = static_cast<std::uint32_t>(output.getVertexCount());
721 for (int radial = 0; radial <= radialSegments; ++radial) {
722 const float angle = static_cast<float>(radial) / static_cast<float>(radialSegments) * tau;
723 output.addVertex(output.getPositionX(static_cast<int>(lastRing) + radial),
724 output.getPositionY(static_cast<int>(lastRing) + radial),
725 output.getPositionZ(static_cast<int>(lastRing) + radial), endNormal.x, endNormal.y,
726 endNormal.z, 0.5f + std::cos(angle) * 0.5f, 0.5f + std::sin(angle) * 0.5f);
727 }
728 for (int radial = 0; radial < radialSegments; ++radial) {
729 const auto r = static_cast<std::uint32_t>(radial);
730 output.addTriangle(startCenter, startRim + r + 1u, startRim + r);
731 output.addTriangle(endCenter, endRim + r, endRim + r + 1u);
732 }
733 }
734 output.setMeta("generator", "mesh.splineTube");
735 output.setMeta("pathSegments", std::to_string(pathSegments));
736 output.setMeta("radialSegments", std::to_string(radialSegments));
737 output.setMeta("closed", node.splinePath.isClosed() ? "1" : "0");
738 return Result<MeshBuild>::success(std::move(output));
739}
740
741Result<MeshBuild> splineRibbonMesh(const auto& node) {
742 if (node.splinePath.chunkCount() > 1) {
743 MeshBuild output;
744 for (int chunk = 0; chunk < node.splinePath.chunkCount(); ++chunk) {
745 auto path = node.splinePath.chunkPathResult(chunk);
746 if (!path.ok()) return Result<MeshBuild>::failure(path.status());
747 auto chunkNode = node;
748 chunkNode.splinePath = std::move(path).takeValue();
749 auto mesh = splineRibbonMesh(chunkNode);
750 if (!mesh.ok()) return mesh;
751 if (!output.appendTransformed(&mesh.value(), 0.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f))
753 "failed to compose spline ribbon chunks", {}, {},
754 "procgen.meshModifierGraph"));
755 }
756 output.setMeta("generator", "mesh.splineRibbon");
757 output.setMeta("chunks", std::to_string(node.splinePath.chunkCount()));
758 return Result<MeshBuild>::success(std::move(output));
759 }
760 const float width = parameter(node, "width", 2.f);
761 const float thickness = parameter(node, "thickness", 0.f);
762 const int pathSegments = intParameter(node, "pathSegments", 32);
763 const bool solid = thickness > 1e-7f;
764 const bool capped = solid && intParameter(node, "cap", 1) != 0 && !node.splinePath.isClosed();
765 auto frames = node.splinePath.sampleFramesResult(pathSegments, true, parameter(node, "roll", 0.f), 24);
766 if (!frames.ok()) return Result<MeshBuild>::failure(frames.status());
767
768 const int ringStride = solid ? 8 : 2;
769 MeshBuild output;
770 output.reserve((pathSegments + 1) * ringStride + (capped ? 8 : 0),
771 pathSegments * (solid ? 24 : 6) + (capped ? 12 : 0));
772 const float halfWidth = width * 0.5f;
773 for (int ring = 0; ring <= pathSegments; ++ring) {
774 const auto& frame = frames.value()[static_cast<std::size_t>(ring)];
775 const auto& p = frame.sample;
776 const Vec3 side{frame.sideX, frame.sideY, frame.sideZ};
777 const Vec3 up{frame.upX, frame.upY, frame.upZ};
778 const float u = static_cast<float>(ring) / static_cast<float>(pathSegments);
779 const float ringHalfWidth = halfWidth * p.scaleX;
780 const float ringThickness = thickness * p.scaleY;
781 const Vec3 left{p.x - side.x * ringHalfWidth, p.y - side.y * ringHalfWidth, p.z - side.z * ringHalfWidth};
782 const Vec3 right{p.x + side.x * ringHalfWidth, p.y + side.y * ringHalfWidth, p.z + side.z * ringHalfWidth};
783 output.addVertex(left.x, left.y, left.z, up.x, up.y, up.z, u, 0.f);
784 output.addVertex(right.x, right.y, right.z, up.x, up.y, up.z, u, 1.f);
785 if (!solid) continue;
786 const Vec3 down{-up.x, -up.y, -up.z};
787 const Vec3 bottomLeft{left.x - up.x * ringThickness, left.y - up.y * ringThickness,
788 left.z - up.z * ringThickness};
789 const Vec3 bottomRight{right.x - up.x * ringThickness, right.y - up.y * ringThickness,
790 right.z - up.z * ringThickness};
791 output.addVertex(bottomRight.x, bottomRight.y, bottomRight.z, down.x, down.y, down.z, u, 0.f);
792 output.addVertex(bottomLeft.x, bottomLeft.y, bottomLeft.z, down.x, down.y, down.z, u, 1.f);
793 output.addVertex(left.x, left.y, left.z, -side.x, -side.y, -side.z, u, 0.f);
794 output.addVertex(bottomLeft.x, bottomLeft.y, bottomLeft.z, -side.x, -side.y, -side.z, u, 1.f);
795 output.addVertex(right.x, right.y, right.z, side.x, side.y, side.z, u, 0.f);
796 output.addVertex(bottomRight.x, bottomRight.y, bottomRight.z, side.x, side.y, side.z, u, 1.f);
797 }
798
799 output.setActiveGroup("surface");
800 for (int ring = 0; ring < pathSegments; ++ring) {
801 const auto a = static_cast<std::uint32_t>(ring * ringStride);
802 const auto b = a + static_cast<std::uint32_t>(ringStride);
803 output.addTriangle(a, b, a + 1u);
804 output.addTriangle(a + 1u, b, b + 1u);
805 }
806 if (solid) {
807 output.setActiveGroup("bottom");
808 for (int ring = 0; ring < pathSegments; ++ring) {
809 const auto a = static_cast<std::uint32_t>(ring * ringStride + 2);
810 const auto b = a + static_cast<std::uint32_t>(ringStride);
811 output.addTriangle(a, b, a + 1u);
812 output.addTriangle(a + 1u, b, b + 1u);
813 }
814 output.setActiveGroup("sides");
815 for (int ring = 0; ring < pathSegments; ++ring) {
816 const auto base = static_cast<std::uint32_t>(ring * ringStride);
817 const auto next = base + static_cast<std::uint32_t>(ringStride);
818 output.addTriangle(base + 4u, base + 5u, next + 4u);
819 output.addTriangle(base + 5u, next + 5u, next + 4u);
820 output.addTriangle(base + 6u, next + 6u, base + 7u);
821 output.addTriangle(base + 7u, next + 6u, next + 7u);
822 }
823 if (capped) {
824 output.setActiveGroup("caps");
825 for (int end = 0; end < 2; ++end) {
826 const int sourceRing = end == 0 ? 0 : pathSegments;
827 const auto& frame = frames.value()[static_cast<std::size_t>(sourceRing)];
828 const Vec3 normal{(end == 0 ? -1.f : 1.f) * frame.sample.tangentX,
829 (end == 0 ? -1.f : 1.f) * frame.sample.tangentY,
830 (end == 0 ? -1.f : 1.f) * frame.sample.tangentZ};
831 const auto source = static_cast<std::uint32_t>(sourceRing * ringStride);
832 const auto base = static_cast<std::uint32_t>(output.getVertexCount());
833 for (const auto offset : {0u, 1u, 2u, 3u}) {
834 const auto vertex = source + offset;
835 output.addVertex(output.getPositionX(static_cast<int>(vertex)),
836 output.getPositionY(static_cast<int>(vertex)),
837 output.getPositionZ(static_cast<int>(vertex)), normal.x, normal.y, normal.z,
838 offset == 0u || offset == 3u ? 0.f : 1.f, offset < 2u ? 0.f : 1.f);
839 }
840 if (end == 0) {
841 output.addTriangle(base, base + 1u, base + 3u);
842 output.addTriangle(base + 1u, base + 2u, base + 3u);
843 } else {
844 output.addTriangle(base, base + 3u, base + 1u);
845 output.addTriangle(base + 1u, base + 3u, base + 2u);
846 }
847 }
848 }
849 }
850 output.setMeta("generator", "mesh.splineRibbon");
851 output.setMeta("pathSegments", std::to_string(pathSegments));
852 output.setMeta("closed", node.splinePath.isClosed() ? "1" : "0");
853 return Result<MeshBuild>::success(std::move(output));
854}
855
856float profileArea(const SplineProfile& profile) {
857 float area = 0.f;
858 for (std::size_t i = 0; i < profile.points.size(); ++i) {
859 const auto& a = profile.points[i];
860 const auto& b = profile.points[(i + 1u) % profile.points.size()];
861 area += a.x * b.y - b.x * a.y;
862 }
863 return area * 0.5f;
864}
865
866float cross2(const SplineProfilePoint& a, const SplineProfilePoint& b, const SplineProfilePoint& c) {
867 return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
868}
869
870bool insideTriangle(const SplineProfilePoint& p, const SplineProfilePoint& a, const SplineProfilePoint& b,
871 const SplineProfilePoint& c) {
872 constexpr float epsilon = 1e-7f;
873 return cross2(a, b, p) >= -epsilon && cross2(b, c, p) >= -epsilon && cross2(c, a, p) >= -epsilon;
874}
875
876Result<std::vector<std::uint32_t>> triangulateProfile(const SplineProfile& profile) {
877 std::vector<std::uint32_t> polygon(profile.points.size());
878 if (profileArea(profile) > 0.f)
879 for (std::size_t i = 0; i < polygon.size(); ++i) polygon[i] = static_cast<std::uint32_t>(i);
880 else
881 for (std::size_t i = 0; i < polygon.size(); ++i)
882 polygon[i] = static_cast<std::uint32_t>(polygon.size() - 1u - i);
883 std::vector<std::uint32_t> triangles;
884 while (polygon.size() > 3u) {
885 bool clipped = false;
886 for (std::size_t i = 0; i < polygon.size(); ++i) {
887 const auto previous = polygon[(i + polygon.size() - 1u) % polygon.size()];
888 const auto current = polygon[i];
889 const auto next = polygon[(i + 1u) % polygon.size()];
890 if (cross2(profile.points[previous], profile.points[current], profile.points[next]) <= 1e-7f) continue;
891 bool contains = false;
892 for (const auto candidate : polygon) {
893 if (candidate == previous || candidate == current || candidate == next) continue;
894 if (insideTriangle(profile.points[candidate], profile.points[previous], profile.points[current],
895 profile.points[next])) {
896 contains = true;
897 break;
898 }
899 }
900 if (contains) continue;
901 triangles.insert(triangles.end(), {previous, current, next});
902 polygon.erase(polygon.begin() + static_cast<std::ptrdiff_t>(i));
903 clipped = true;
904 break;
905 }
906 if (!clipped)
907 return Result<std::vector<std::uint32_t>>::failure(Diagnostic::error(
908 DiagnosticCode::InvalidArgument, "closed spline profile must be a simple non-degenerate polygon",
909 "profile", {}, "procgen.meshModifierGraph"));
910 }
911 triangles.insert(triangles.end(), {polygon[0], polygon[1], polygon[2]});
912 return Result<std::vector<std::uint32_t>>::success(std::move(triangles));
913}
914
915Result<MeshBuild> splineExtrudeMesh(const auto& node) {
916 if (node.splinePath.chunkCount() > 1) {
917 MeshBuild output;
918 for (int chunk = 0; chunk < node.splinePath.chunkCount(); ++chunk) {
919 auto path = node.splinePath.chunkPathResult(chunk);
920 if (!path.ok()) return Result<MeshBuild>::failure(path.status());
921 auto chunkNode = node;
922 chunkNode.splinePath = std::move(path).takeValue();
923 auto mesh = splineExtrudeMesh(chunkNode);
924 if (!mesh.ok()) return mesh;
925 if (!output.appendTransformed(&mesh.value(), 0.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f))
927 "failed to compose spline extrusion chunks", {}, {},
928 "procgen.meshModifierGraph"));
929 }
930 output.setMeta("generator", "mesh.splineExtrude");
931 output.setMeta("chunks", std::to_string(node.splinePath.chunkCount()));
932 return Result<MeshBuild>::success(std::move(output));
933 }
934 const int pathSegments = intParameter(node, "pathSegments", 32);
935 const bool capped = node.splineProfile.closed && intParameter(node, "cap", 1) != 0 && !node.splinePath.isClosed();
936 auto frames = node.splinePath.sampleFramesResult(pathSegments, true, parameter(node, "roll", 0.f), 24);
937 if (!frames.ok()) return Result<MeshBuild>::failure(frames.status());
938 const auto& profile = node.splineProfile;
939 const int pointCount = static_cast<int>(profile.points.size());
940 const int profileSegments = profile.closed ? pointCount : pointCount - 1;
941 const int ringStride = pointCount + (profile.closed ? 1 : 0);
942 MeshBuild output;
943 output.reserve((pathSegments + 1) * ringStride + (capped ? pointCount * 2 : 0),
944 pathSegments * profileSegments * 6 + (capped ? (pointCount - 2) * 6 : 0));
945 const float orientation = profile.closed && profileArea(profile) < 0.f ? -1.f : 1.f;
946 for (int ring = 0; ring <= pathSegments; ++ring) {
947 const auto& frame = frames.value()[static_cast<std::size_t>(ring)];
948 const auto& sample = frame.sample;
949 for (int profileIndex = 0; profileIndex < ringStride; ++profileIndex) {
950 const int i = profileIndex % pointCount;
951 const int previous = profile.closed ? (i + pointCount - 1) % pointCount : std::max(0, i - 1);
952 const int next = profile.closed ? (i + 1) % pointCount : std::min(pointCount - 1, i + 1);
953 const auto& p = profile.points[static_cast<std::size_t>(i)];
954 const auto& a = profile.points[static_cast<std::size_t>(previous)];
955 const auto& b = profile.points[static_cast<std::size_t>(next)];
956 float nx = orientation * (b.y - a.y), ny = orientation * (a.x - b.x);
957 const float inverseX = 1.f / sample.scaleX, inverseY = 1.f / sample.scaleY;
958 nx *= inverseX;
959 ny *= inverseY;
960 const float nl = std::sqrt(nx * nx + ny * ny);
961 nx /= nl;
962 ny /= nl;
963 output.addVertex(sample.x + frame.sideX * p.x * sample.scaleX + frame.upX * p.y * sample.scaleY,
964 sample.y + frame.sideY * p.x * sample.scaleX + frame.upY * p.y * sample.scaleY,
965 sample.z + frame.sideZ * p.x * sample.scaleX + frame.upZ * p.y * sample.scaleY,
966 frame.sideX * nx + frame.upX * ny, frame.sideY * nx + frame.upY * ny,
967 frame.sideZ * nx + frame.upZ * ny,
968 static_cast<float>(ring) / static_cast<float>(pathSegments),
969 static_cast<float>(profileIndex) / static_cast<float>(profileSegments));
970 }
971 }
972 output.setActiveGroup("profile");
973 for (int ring = 0; ring < pathSegments; ++ring)
974 for (int segment = 0; segment < profileSegments; ++segment) {
975 const auto a = static_cast<std::uint32_t>(ring * ringStride + segment);
976 const auto b = a + static_cast<std::uint32_t>(ringStride);
977 output.addTriangle(a, a + 1u, b);
978 output.addTriangle(a + 1u, b + 1u, b);
979 }
980 if (capped) {
981 auto triangles = triangulateProfile(profile);
982 if (!triangles.ok()) return Result<MeshBuild>::failure(triangles.status());
983 output.setActiveGroup("caps");
984 for (int end = 0; end < 2; ++end) {
985 const int ring = end == 0 ? 0 : pathSegments;
986 const auto& frame = frames.value()[static_cast<std::size_t>(ring)];
987 const auto base = static_cast<std::uint32_t>(output.getVertexCount());
988 for (int i = 0; i < pointCount; ++i) {
989 const int source = ring * ringStride + i;
990 const float direction = end == 0 ? -1.f : 1.f;
991 output.addVertex(output.getPositionX(source), output.getPositionY(source), output.getPositionZ(source),
992 frame.sample.tangentX * direction, frame.sample.tangentY * direction,
993 frame.sample.tangentZ * direction, profile.points[static_cast<std::size_t>(i)].x,
994 profile.points[static_cast<std::size_t>(i)].y);
995 }
996 for (std::size_t i = 0; i < triangles.value().size(); i += 3u) {
997 const auto a = base + triangles.value()[i], b = base + triangles.value()[i + 1u];
998 const auto c = base + triangles.value()[i + 2u];
999 if (end == 0)
1000 output.addTriangle(a, c, b);
1001 else
1002 output.addTriangle(a, b, c);
1003 }
1004 }
1005 }
1006 output.setMeta("generator", "mesh.splineExtrude");
1007 output.setMeta("profileClosed", profile.closed ? "1" : "0");
1008 output.setMeta("closed", node.splinePath.isClosed() ? "1" : "0");
1009 return Result<MeshBuild>::success(std::move(output));
1010}
1011
1012bool rayTriangle(Vec3 origin, Vec3 direction, Vec3 a, Vec3 b, Vec3 c, float& distance, Vec3& normal) {
1013 const Vec3 edge1{b.x - a.x, b.y - a.y, b.z - a.z};
1014 const Vec3 edge2{c.x - a.x, c.y - a.y, c.z - a.z};
1015 const Vec3 p{direction.y * edge2.z - direction.z * edge2.y, direction.z * edge2.x - direction.x * edge2.z,
1016 direction.x * edge2.y - direction.y * edge2.x};
1017 const float determinant = edge1.x * p.x + edge1.y * p.y + edge1.z * p.z;
1018 if (std::abs(determinant) < 1e-7f) return false;
1019 const float inverse = 1.f / determinant;
1020 const Vec3 offset{origin.x - a.x, origin.y - a.y, origin.z - a.z};
1021 const float u = (offset.x * p.x + offset.y * p.y + offset.z * p.z) * inverse;
1022 if (u < 0.f || u > 1.f) return false;
1023 const Vec3 q{offset.y * edge1.z - offset.z * edge1.y, offset.z * edge1.x - offset.x * edge1.z,
1024 offset.x * edge1.y - offset.y * edge1.x};
1025 const float v = (direction.x * q.x + direction.y * q.y + direction.z * q.z) * inverse;
1026 if (v < 0.f || u + v > 1.f) return false;
1027 distance = (edge2.x * q.x + edge2.y * q.y + edge2.z * q.z) * inverse;
1028 if (distance < 1e-6f) return false;
1029 normal = {edge1.y * edge2.z - edge1.z * edge2.y, edge1.z * edge2.x - edge1.x * edge2.z,
1030 edge1.x * edge2.y - edge1.y * edge2.x};
1031 const float normalLength = std::sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
1032 if (normalLength < 1e-7f) return false;
1033 normal = {normal.x / normalLength, normal.y / normalLength, normal.z / normalLength};
1034 return true;
1035}
1036
1037Result<MeshBuild> meshFitMesh(const MeshBuild& input, const MeshBuild& surface, const auto& node) {
1038 MeshBuild output = input;
1039 auto& positions = output.positions();
1040 Vec3 direction{parameter(node, "directionX", 0.f), parameter(node, "directionY", -1.f),
1041 parameter(node, "directionZ", 0.f)};
1042 const float directionLength =
1043 std::sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z);
1044 direction = {direction.x / directionLength, direction.y / directionLength, direction.z / directionLength};
1045 const float maximum = parameter(node, "maxDistance", 10.f);
1046 const float surfaceOffset = parameter(node, "surfaceOffset", 0.f);
1047 const bool bidirectional = intParameter(node, "bidirectional", 0) != 0;
1048 const auto& targetPositions = surface.positions();
1049 const auto& targetIndices = surface.indices();
1050 for (int vertex = 0; vertex < output.getVertexCount(); ++vertex) {
1051 const std::size_t base = static_cast<std::size_t>(vertex) * 3u;
1052 const Vec3 origin{positions[base], positions[base + 1], positions[base + 2]};
1053 float best = maximum + 1.f;
1054 Vec3 bestDirection{}, bestNormal{};
1055 for (const float sign : {1.f, -1.f}) {
1056 if (sign < 0.f && !bidirectional) continue;
1057 const Vec3 ray{direction.x * sign, direction.y * sign, direction.z * sign};
1058 for (std::size_t triangle = 0; triangle + 2u < targetIndices.size(); triangle += 3u) {
1059 const auto point = [&](std::uint32_t index) {
1060 const std::size_t target = static_cast<std::size_t>(index) * 3u;
1061 return Vec3{targetPositions[target], targetPositions[target + 1], targetPositions[target + 2]};
1062 };
1063 float distance = 0.f;
1064 Vec3 normal{};
1065 if (!rayTriangle(origin, ray, point(targetIndices[triangle]), point(targetIndices[triangle + 1u]),
1066 point(targetIndices[triangle + 2u]), distance, normal) ||
1067 distance > maximum || distance >= best)
1068 continue;
1069 best = distance;
1070 bestDirection = ray;
1071 bestNormal = normal;
1072 }
1073 }
1074 if (best > maximum) continue;
1075 positions[base] = origin.x + bestDirection.x * best + bestNormal.x * surfaceOffset;
1076 positions[base + 1] = origin.y + bestDirection.y * best + bestNormal.y * surfaceOffset;
1077 positions[base + 2] = origin.z + bestDirection.z * best + bestNormal.z * surfaceOffset;
1078 }
1079 recalculateNormals(output);
1080 output.setMeta("deformer", "deform.meshFit");
1081 return Result<MeshBuild>::success(std::move(output));
1082}
1083
1084Result<MeshBuild> ffdMesh(const MeshBuild& input, const auto& node) {
1085 MeshBuild output = input;
1086 auto& positions = output.positions();
1087 if (positions.empty()) return Result<MeshBuild>::success(std::move(output));
1090 for (std::size_t i = 0; i < positions.size(); i += 3u) {
1091 minimum = {std::min(minimum.x, positions[i]), std::min(minimum.y, positions[i + 1]),
1092 std::min(minimum.z, positions[i + 2])};
1093 maximum = {std::max(maximum.x, positions[i]), std::max(maximum.y, positions[i + 1]),
1094 std::max(maximum.z, positions[i + 2])};
1095 }
1096 const Vec3 extent{std::max(maximum.x - minimum.x, 1e-6f), std::max(maximum.y - minimum.y, 1e-6f),
1097 std::max(maximum.z - minimum.z, 1e-6f)};
1098 std::array<Vec3, 8> offsets;
1099 const std::array<const char*, 8> names{"p000", "p001", "p010", "p011", "p100", "p101", "p110", "p111"};
1100 for (std::size_t i = 0; i < names.size(); ++i) {
1101 offsets[i] = {parameter(node, std::string(names[i]) + "x", 0.f),
1102 parameter(node, std::string(names[i]) + "y", 0.f),
1103 parameter(node, std::string(names[i]) + "z", 0.f)};
1104 }
1105 const float weight = std::clamp(parameter(node, "weight", 1.f), 0.f, 1.f);
1106 for (std::size_t base = 0; base < positions.size(); base += 3u) {
1107 const float x = std::clamp((positions[base] - minimum.x) / extent.x, 0.f, 1.f);
1108 const float y = std::clamp((positions[base + 1] - minimum.y) / extent.y, 0.f, 1.f);
1109 const float z = std::clamp((positions[base + 2] - minimum.z) / extent.z, 0.f, 1.f);
1110 Vec3 displacement;
1111 for (int ix = 0; ix < 2; ++ix)
1112 for (int iy = 0; iy < 2; ++iy)
1113 for (int iz = 0; iz < 2; ++iz) {
1114 const float blend = (ix ? x : 1.f - x) * (iy ? y : 1.f - y) * (iz ? z : 1.f - z);
1115 const auto& offset = offsets[static_cast<std::size_t>(ix * 4 + iy * 2 + iz)];
1116 displacement.x += offset.x * blend;
1117 displacement.y += offset.y * blend;
1118 displacement.z += offset.z * blend;
1119 }
1120 positions[base] += displacement.x * weight;
1121 positions[base + 1] += displacement.y * weight;
1122 positions[base + 2] += displacement.z * weight;
1123 }
1124 recalculateNormals(output);
1125 return Result<MeshBuild>::success(std::move(output));
1126}
1127
1128Result<MeshBuild> morphMesh(const MeshBuild& first, const MeshBuild& second, float weight) {
1129 if (first.getVertexCount() != second.getVertexCount() || first.indices() != second.indices())
1131 "deform.morph inputs must have identical topology",
1132 "deform.morph", {}, "procgen.meshModifierGraph"));
1133 MeshBuild output = first;
1134 weight = std::clamp(weight, 0.f, 1.f);
1135 for (std::size_t i = 0; i < output.positions().size(); ++i)
1136 output.positions()[i] = std::lerp(first.positions()[i], second.positions()[i], weight);
1137 recalculateNormals(output);
1138 return Result<MeshBuild>::success(std::move(output));
1139}
1140
1141Result<MeshBuild> subdivideMesh(const MeshBuild& input, int levels) {
1142 MeshBuild current = input;
1143 levels = std::clamp(levels, 1, 4);
1144 for (int level = 0; level < levels; ++level) {
1145 MeshBuild next;
1146 next.reserve(current.getIndexCount() * 2, current.getIndexCount() * 4);
1147 std::vector<int> assignments;
1148 auto addSource = [&](std::uint32_t index) {
1149 next.addVertex(current.getPositionX(static_cast<int>(index)), current.getPositionY(static_cast<int>(index)),
1150 current.getPositionZ(static_cast<int>(index)), current.getNormalX(static_cast<int>(index)),
1151 current.getNormalY(static_cast<int>(index)), current.getNormalZ(static_cast<int>(index)),
1152 current.getUvU(static_cast<int>(index)), current.getUvV(static_cast<int>(index)));
1153 return static_cast<std::uint32_t>(next.getVertexCount() - 1);
1154 };
1155 auto midpoint = [&](std::uint32_t a, std::uint32_t b) {
1156 next.addVertex((current.getPositionX(a) + current.getPositionX(b)) * 0.5f,
1157 (current.getPositionY(a) + current.getPositionY(b)) * 0.5f,
1158 (current.getPositionZ(a) + current.getPositionZ(b)) * 0.5f,
1159 (current.getNormalX(a) + current.getNormalX(b)) * 0.5f,
1160 (current.getNormalY(a) + current.getNormalY(b)) * 0.5f,
1161 (current.getNormalZ(a) + current.getNormalZ(b)) * 0.5f,
1162 (current.getUvU(a) + current.getUvU(b)) * 0.5f,
1163 (current.getUvV(a) + current.getUvV(b)) * 0.5f);
1164 return static_cast<std::uint32_t>(next.getVertexCount() - 1);
1165 };
1166 for (int triangle = 0; triangle * 3 + 2 < current.getIndexCount(); ++triangle) {
1167 const auto a0 = static_cast<std::uint32_t>(current.getIndex(triangle * 3));
1168 const auto b0 = static_cast<std::uint32_t>(current.getIndex(triangle * 3 + 1));
1169 const auto c0 = static_cast<std::uint32_t>(current.getIndex(triangle * 3 + 2));
1170 const auto a = addSource(a0), b = addSource(b0), c = addSource(c0);
1171 const auto ab = midpoint(a0, b0), bc = midpoint(b0, c0), ca = midpoint(c0, a0);
1172 for (const auto& tri :
1173 {std::array{a, ab, ca}, std::array{ab, b, bc}, std::array{ca, bc, c}, std::array{ab, bc, ca}}) {
1174 next.addTriangle(tri[0], tri[1], tri[2]);
1175 assignments.push_back(current.getTriangleGroup(triangle));
1176 }
1177 }
1178 std::vector<std::string> names;
1179 for (int i = 0; i < current.getGroupCount(); ++i) names.push_back(current.getGroupName(i));
1180 auto restored = next.restoreGroupData(std::move(names), std::move(assignments), -1);
1181 if (!restored.ok()) return Result<MeshBuild>::failure(restored.status());
1182 copyMetadata(current, next);
1183 recalculateNormals(next);
1184 current = std::move(next);
1185 }
1186 return Result<MeshBuild>::success(std::move(current));
1187}
1188
1189struct ClipVertex {
1192 float u = 0.f;
1193 float v = 0.f;
1194};
1195
1196ClipVertex interpolate(const ClipVertex& a, const ClipVertex& b, float t) {
1197 return {{std::lerp(a.position.x, b.position.x, t), std::lerp(a.position.y, b.position.y, t),
1198 std::lerp(a.position.z, b.position.z, t)},
1199 {std::lerp(a.normal.x, b.normal.x, t), std::lerp(a.normal.y, b.normal.y, t),
1200 std::lerp(a.normal.z, b.normal.z, t)},
1201 std::lerp(a.u, b.u, t),
1202 std::lerp(a.v, b.v, t)};
1203}
1204
1205Result<MeshBuild> cutPlaneMesh(const MeshBuild& input, const auto& node) {
1206 Vec3 normal{parameter(node, "normalX", 0.f), parameter(node, "normalY", 1.f), parameter(node, "normalZ", 0.f)};
1207 const float length = std::sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
1208 if (length < 1e-7f)
1210 "mesh.cutPlane requires a non-zero normal", "normal", {},
1211 "procgen.meshModifierGraph"));
1212 normal = {normal.x / length, normal.y / length, normal.z / length};
1213 const float distance = parameter(node, "distance", 0.f);
1214 const float sign = intParameter(node, "keepPositive", 1) != 0 ? 1.f : -1.f;
1215 auto signedDistance = [&](const ClipVertex& vertex) {
1216 return sign *
1217 (vertex.position.x * normal.x + vertex.position.y * normal.y + vertex.position.z * normal.z - distance);
1218 };
1219 MeshBuild output;
1220 output.reserve(input.getIndexCount() * 2, input.getIndexCount() * 2);
1221 std::vector<int> assignments;
1222 std::vector<std::array<Vec3, 2>> cutSegments;
1223 for (int triangle = 0; triangle * 3 + 2 < input.getIndexCount(); ++triangle) {
1224 std::vector<ClipVertex> polygon;
1225 for (int corner = 0; corner < 3; ++corner) {
1226 const int index = input.getIndex(triangle * 3 + corner);
1227 polygon.push_back({{input.getPositionX(index), input.getPositionY(index), input.getPositionZ(index)},
1228 {input.getNormalX(index), input.getNormalY(index), input.getNormalZ(index)},
1229 input.getUvU(index),
1230 input.getUvV(index)});
1231 }
1232 std::vector<ClipVertex> clipped;
1233 std::vector<Vec3> intersections;
1234 for (std::size_t i = 0; i < polygon.size(); ++i) {
1235 const auto& a = polygon[i];
1236 const auto& b = polygon[(i + 1u) % polygon.size()];
1237 const float da = signedDistance(a), db = signedDistance(b);
1238 const bool insideA = da >= 0.f, insideB = db >= 0.f;
1239 if (insideA) clipped.push_back(a);
1240 if (insideA != insideB) {
1241 auto intersection = interpolate(a, b, da / (da - db));
1242 clipped.push_back(intersection);
1243 intersections.push_back(intersection.position);
1244 }
1245 }
1246 if (intersections.size() == 2u) cutSegments.push_back({intersections[0], intersections[1]});
1247 if (clipped.size() < 3u) continue;
1248 const auto base = static_cast<std::uint32_t>(output.getVertexCount());
1249 for (const auto& vertex : clipped)
1250 output.addVertex(vertex.position.x, vertex.position.y, vertex.position.z, vertex.normal.x, vertex.normal.y,
1251 vertex.normal.z, vertex.u, vertex.v);
1252 for (std::size_t i = 1; i + 1 < clipped.size(); ++i) {
1253 output.addTriangle(base, base + static_cast<std::uint32_t>(i), base + static_cast<std::uint32_t>(i + 1));
1254 assignments.push_back(input.getTriangleGroup(triangle));
1255 }
1256 }
1257 std::vector<std::string> names;
1258 for (int i = 0; i < input.getGroupCount(); ++i) names.push_back(input.getGroupName(i));
1259 if (intParameter(node, "cap", 0) != 0 && !cutSegments.empty()) {
1260 const int capGroup = static_cast<int>(names.size());
1261 names.push_back("__cut_cap");
1262 std::vector<Vec3> points;
1263 std::vector<std::array<int, 2>> edges;
1264 auto pointIndex = [&](const Vec3& point) {
1265 constexpr float toleranceSquared = 1e-10f;
1266 for (std::size_t i = 0; i < points.size(); ++i) {
1267 const float dx = points[i].x - point.x, dy = points[i].y - point.y, dz = points[i].z - point.z;
1268 if (dx * dx + dy * dy + dz * dz <= toleranceSquared) return static_cast<int>(i);
1269 }
1270 points.push_back(point);
1271 return static_cast<int>(points.size() - 1u);
1272 };
1273 for (const auto& segment : cutSegments) {
1274 const int a = pointIndex(segment[0]), b = pointIndex(segment[1]);
1275 if (a == b) continue;
1276 const auto duplicate = std::find_if(edges.begin(), edges.end(), [&](const auto& edge) {
1277 return (edge[0] == a && edge[1] == b) || (edge[0] == b && edge[1] == a);
1278 });
1279 if (duplicate == edges.end()) edges.push_back({a, b});
1280 }
1281 std::vector<bool> used(edges.size(), false);
1282 for (std::size_t seed = 0; seed < edges.size(); ++seed) {
1283 if (used[seed]) continue;
1284 std::vector<int> loop{edges[seed][0], edges[seed][1]};
1285 used[seed] = true;
1286 while (loop.back() != loop.front()) {
1287 bool extended = false;
1288 for (std::size_t edge = 0; edge < edges.size(); ++edge) {
1289 if (used[edge]) continue;
1290 if (edges[edge][0] == loop.back() || edges[edge][1] == loop.back()) {
1291 loop.push_back(edges[edge][0] == loop.back() ? edges[edge][1] : edges[edge][0]);
1292 used[edge] = true;
1293 extended = true;
1294 break;
1295 }
1296 }
1297 if (!extended || loop.size() > edges.size() + 1u) break;
1298 }
1299 if (loop.size() < 4u || loop.back() != loop.front()) continue;
1300 loop.pop_back();
1301 Vec3 centroid;
1302 for (const int index : loop) {
1303 centroid.x += points[static_cast<std::size_t>(index)].x;
1304 centroid.y += points[static_cast<std::size_t>(index)].y;
1305 centroid.z += points[static_cast<std::size_t>(index)].z;
1306 }
1307 const float inverseCount = 1.f / static_cast<float>(loop.size());
1308 centroid = {centroid.x * inverseCount, centroid.y * inverseCount, centroid.z * inverseCount};
1309 const Vec3 capNormal{-normal.x * sign, -normal.y * sign, -normal.z * sign};
1310 Vec3 winding;
1311 for (std::size_t i = 0; i < loop.size(); ++i) {
1312 const auto& a = points[static_cast<std::size_t>(loop[i])];
1313 const auto& b = points[static_cast<std::size_t>(loop[(i + 1u) % loop.size()])];
1314 winding.x += a.y * b.z - a.z * b.y;
1315 winding.y += a.z * b.x - a.x * b.z;
1316 winding.z += a.x * b.y - a.y * b.x;
1317 }
1318 if (winding.x * capNormal.x + winding.y * capNormal.y + winding.z * capNormal.z < 0.f)
1319 std::reverse(loop.begin(), loop.end());
1320 const auto centerIndex = static_cast<std::uint32_t>(output.getVertexCount());
1321 output.addVertex(centroid.x, centroid.y, centroid.z, capNormal.x, capNormal.y, capNormal.z, 0.5f, 0.5f);
1322 for (const int index : loop) {
1323 const auto& point = points[static_cast<std::size_t>(index)];
1324 output.addVertex(point.x, point.y, point.z, capNormal.x, capNormal.y, capNormal.z,
1325 0.5f + (point.x - centroid.x), 0.5f + (point.z - centroid.z));
1326 }
1327 for (std::size_t i = 0; i < loop.size(); ++i) {
1328 const auto a = centerIndex + 1u + static_cast<std::uint32_t>(i);
1329 const auto b = centerIndex + 1u + static_cast<std::uint32_t>((i + 1u) % loop.size());
1330 output.addTriangle(centerIndex, a, b);
1331 assignments.push_back(capGroup);
1332 }
1333 }
1334 }
1335 auto restored = output.restoreGroupData(std::move(names), std::move(assignments), -1);
1336 if (!restored.ok()) return Result<MeshBuild>::failure(restored.status());
1337 copyMetadata(input, output);
1338 return Result<MeshBuild>::success(std::move(output));
1339}
1340
1341struct WeldKey {
1342 std::int64_t x, y, z;
1343 bool operator==(const WeldKey&) const = default;
1344};
1345
1346struct WeldKeyHash {
1347 std::size_t operator()(const WeldKey& key) const noexcept {
1348 std::size_t value = std::hash<std::int64_t>{}(key.x);
1349 value ^= std::hash<std::int64_t>{}(key.y) + 0x9e3779b9u + (value << 6u) + (value >> 2u);
1350 value ^= std::hash<std::int64_t>{}(key.z) + 0x9e3779b9u + (value << 6u) + (value >> 2u);
1351 return value;
1352 }
1353};
1354
1355Result<MeshBuild> weldMesh(const MeshBuild& input, float tolerance) {
1356 if (!std::isfinite(tolerance) || tolerance <= 0.f)
1358 "mesh.weld tolerance must be finite and positive",
1359 "tolerance", {}, "procgen.meshModifierGraph"));
1360 MeshBuild output;
1361 output.reserve(input.getVertexCount(), input.getIndexCount());
1362 std::unordered_map<WeldKey, std::uint32_t, WeldKeyHash> vertices;
1363 std::vector<std::uint32_t> remap(static_cast<std::size_t>(input.getVertexCount()));
1364 for (int i = 0; i < input.getVertexCount(); ++i) {
1365 const WeldKey key{std::llround(input.getPositionX(i) / tolerance),
1366 std::llround(input.getPositionY(i) / tolerance),
1367 std::llround(input.getPositionZ(i) / tolerance)};
1368 const auto found = vertices.find(key);
1369 if (found != vertices.end()) {
1370 remap[static_cast<std::size_t>(i)] = found->second;
1371 continue;
1372 }
1373 const auto next = static_cast<std::uint32_t>(output.getVertexCount());
1374 vertices.emplace(key, next);
1375 remap[static_cast<std::size_t>(i)] = next;
1376 output.addVertex(input.getPositionX(i), input.getPositionY(i), input.getPositionZ(i), input.getNormalX(i),
1377 input.getNormalY(i), input.getNormalZ(i), input.getUvU(i), input.getUvV(i));
1378 }
1379 std::vector<int> assignments;
1380 for (int i = 0; i + 2 < input.getIndexCount(); i += 3) {
1381 const auto a = remap[static_cast<std::size_t>(input.getIndex(i))];
1382 const auto b = remap[static_cast<std::size_t>(input.getIndex(i + 1))];
1383 const auto c = remap[static_cast<std::size_t>(input.getIndex(i + 2))];
1384 if (a == b || b == c || a == c) continue;
1385 output.addTriangle(a, b, c);
1386 assignments.push_back(input.getTriangleGroup(i / 3));
1387 }
1388 std::vector<std::string> names;
1389 for (int i = 0; i < input.getGroupCount(); ++i) names.push_back(input.getGroupName(i));
1390 auto restored = output.restoreGroupData(std::move(names), std::move(assignments), -1);
1391 if (!restored.ok()) return Result<MeshBuild>::failure(restored.status());
1392 if (input.hasVertexColors()) {
1393 std::vector<float> colors(static_cast<std::size_t>(output.getVertexCount()) * 4u, 1.f);
1394 for (int i = 0; i < input.getVertexCount(); ++i) {
1395 const auto dst = remap[static_cast<std::size_t>(i)];
1396 for (int c = 0; c < 4; ++c)
1397 colors[static_cast<std::size_t>(dst) * 4u + static_cast<std::size_t>(c)] = input.getColor(i, c);
1398 }
1399 auto set = output.setVertexColors(std::move(colors));
1400 if (!set.ok()) return Result<MeshBuild>::failure(set.status());
1401 }
1402 for (const auto& [key, value] : input.metadata()) output.setMeta(key, value);
1404 return Result<MeshBuild>::success(std::move(output));
1405}
1406
1407} // namespace
1408
1410 if (id.empty()) return Result<void>::failure(
1411 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh modifier node id is empty", "node.id", {}, "procgen.meshModifierGraph"));
1412 const auto* spec = findSpec(operation);
1413 if (!spec)
1414 return Result<void>::failure(
1415 Diagnostic::error(DiagnosticCode::NotFound, "unknown mesh modifier operation: " + operation, "node.operation", {}, "procgen.meshModifierGraph"));
1416 if (nodes_.contains(id))
1417 return Result<void>::failure(
1418 Diagnostic::error(DiagnosticCode::AlreadyExists, "duplicate mesh modifier node: " + id, "node.id", {}, "procgen.meshModifierGraph"));
1419 Node node;
1420 node.id = id;
1421 node.operation = std::move(operation);
1422 node.inputs.resize(static_cast<std::size_t>(spec->inputs));
1423 for (const auto& param : spec->params) {
1424 if (std::string_view(param.kind) == "float")
1425 node.floats[param.key] = std::stof(param.defaultValue);
1426 else if (std::string_view(param.kind) == "int")
1427 node.ints[param.key] = std::stoi(param.defaultValue);
1428 else
1429 node.strings[param.key] = param.defaultValue;
1430 }
1431 nodes_.emplace(id, std::move(node));
1432 nodeOrder_.push_back(std::move(id));
1433 invalidate();
1434 return Result<void>::success();
1435}
1436
1438 const std::string key(id);
1439 if (!nodes_.erase(key))
1440 return Result<void>::failure(
1441 Diagnostic::error(DiagnosticCode::NotFound, "mesh modifier node not found: " + key, "node.id", {}, "procgen.meshModifierGraph"));
1442 std::erase(nodeOrder_, key);
1443 for (auto& [_, node] : nodes_)
1444 for (auto& input : node.inputs)
1445 if (input == key) input.clear();
1446 invalidate();
1447 return Result<void>::success();
1448}
1449
1450Result<void> MeshModifierGraph::connect(std::string_view fromId, std::string_view toId, int inputIndex) {
1451 const auto source = nodes_.find(std::string(fromId));
1452 const auto target = nodes_.find(std::string(toId));
1453 if (source == nodes_.end() || target == nodes_.end())
1454 return Result<void>::failure(
1455 Diagnostic::error(DiagnosticCode::NotFound, "mesh modifier connection references an unknown node", "edge", {}, "procgen.meshModifierGraph"));
1456 if (source == target)
1457 return Result<void>::failure(
1458 Diagnostic::error(DiagnosticCode::Conflict, "mesh modifier node cannot connect to itself", "edge", {}, "procgen.meshModifierGraph"));
1459 if (inputIndex < 0 || inputIndex >= static_cast<int>(target->second.inputs.size()))
1460 return Result<void>::failure(
1461 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh modifier input index is out of range", "edge.input", {}, "procgen.meshModifierGraph"));
1462 target->second.inputs[static_cast<std::size_t>(inputIndex)] = source->first;
1463 invalidate();
1464 return Result<void>::success();
1465}
1466
1467Result<void> MeshModifierGraph::disconnect(std::string_view toId, int inputIndex) {
1468 const auto target = nodes_.find(std::string(toId));
1469 if (target == nodes_.end())
1470 return Result<void>::failure(
1471 Diagnostic::error(DiagnosticCode::NotFound, "mesh modifier node not found", "edge.to", {}, "procgen.meshModifierGraph"));
1472 if (inputIndex < 0 || inputIndex >= static_cast<int>(target->second.inputs.size()))
1473 return Result<void>::failure(
1474 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh modifier input index is out of range", "edge.input", {}, "procgen.meshModifierGraph"));
1475 target->second.inputs[static_cast<std::size_t>(inputIndex)].clear();
1476 invalidate();
1477 return Result<void>::success();
1478}
1479
1481 const auto found = nodes_.find(std::string(id));
1482 if (found == nodes_.end()) return Result<void>::failure(
1483 Diagnostic::error(DiagnosticCode::NotFound, "mesh modifier node not found", "node.id", {}, "procgen.meshModifierGraph"));
1484 if (found->second.operation != "mesh.input")
1485 return Result<void>::failure(
1486 Diagnostic::error(DiagnosticCode::PreconditionViolation, "only mesh.input accepts a mesh snapshot", "node.operation", {}, "procgen.meshModifierGraph"));
1487 auto valid = validateMesh(mesh, "mesh");
1488 if (!valid.ok()) return valid;
1489 found->second.inputMesh = mesh;
1490 found->second.hasInputMesh = true;
1491 invalidate();
1492 return Result<void>::success();
1493}
1494
1496 const auto found = nodes_.find(std::string(id));
1497 if (found == nodes_.end()) return Result<void>::failure(
1498 Diagnostic::error(DiagnosticCode::NotFound, "mesh modifier node not found", "node.id", {}, "procgen.meshModifierGraph"));
1499 if (found->second.operation != "deform.splinePath" && found->second.operation != "mesh.splineTube" &&
1500 found->second.operation != "mesh.splineRibbon" && found->second.operation != "mesh.splineExtrude")
1501 return Result<void>::failure(
1502 Diagnostic::error(DiagnosticCode::TypeMismatch, "spline paths can only be bound to spline deformation or generation nodes", std::string(id), {}, "procgen.meshModifierGraph"));
1503 auto ready = path.evaluateResult(0.f);
1504 if (!ready.ok()) return Result<void>::failure(ready.status());
1505 found->second.splinePath = path;
1506 found->second.hasSplinePath = true;
1507 invalidate();
1508 return Result<void>::success();
1509}
1510
1512 const auto found = nodes_.find(std::string(id));
1513 if (found == nodes_.end()) return Result<void>::failure(
1514 Diagnostic::error(DiagnosticCode::NotFound, "mesh modifier node not found", "node.id", {}, "procgen.meshModifierGraph"));
1515 if (found->second.operation != "mesh.splineExtrude")
1516 return Result<void>::failure(
1517 Diagnostic::error(DiagnosticCode::TypeMismatch, "profiles can only be bound to mesh.splineExtrude", std::string(id), {}, "procgen.meshModifierGraph"));
1518 const std::size_t minimum = profile.closed ? 3u : 2u;
1519 if (profile.points.size() < minimum || profile.points.size() > 256u)
1520 return Result<void>::failure(
1521 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline profile point count is out of range", "profile.points", {}, "procgen.meshModifierGraph"));
1522 for (std::size_t i = 0; i < profile.points.size(); ++i) {
1523 const auto& point = profile.points[i];
1524 const auto& next = profile.points[(i + 1u) % profile.points.size()];
1525 if (!std::isfinite(point.x) || !std::isfinite(point.y))
1526 return Result<void>::failure(
1527 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline profile points must be finite", "profile.points", {}, "procgen.meshModifierGraph"));
1528 if ((profile.closed || i + 1u < profile.points.size()) &&
1529 std::hypot(next.x - point.x, next.y - point.y) < 1e-6f)
1530 return Result<void>::failure(
1531 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline profile edges must have non-zero length", "profile.points", {}, "procgen.meshModifierGraph"));
1532 }
1533 if (profile.closed) {
1534 auto triangles = triangulateProfile(profile);
1535 if (!triangles.ok()) return Result<void>::failure(triangles.status());
1536 }
1537 found->second.splineProfile = profile;
1538 found->second.hasSplineProfile = true;
1539 invalidate();
1540 return Result<void>::success();
1541}
1542
1543Result<void> MeshModifierGraph::setNodeFloat(std::string_view id, std::string key, float value) {
1544 if (!std::isfinite(value))
1545 return Result<void>::failure(
1546 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh modifier float must be finite", key, {}, "procgen.meshModifierGraph"));
1547 const auto found = nodes_.find(std::string(id));
1548 if (found == nodes_.end()) return Result<void>::failure(
1549 Diagnostic::error(DiagnosticCode::NotFound, "mesh modifier node not found", "node.id", {}, "procgen.meshModifierGraph"));
1550 const auto* spec = findSpec(found->second.operation);
1551 const auto* param = spec ? findParam(*spec, key) : nullptr;
1552 if (!param || std::string_view(param->kind) != "float")
1553 return Result<void>::failure(
1554 Diagnostic::error(DiagnosticCode::TypeMismatch, "unknown or non-float mesh modifier parameter", key, {}, "procgen.meshModifierGraph"));
1555 found->second.floats[std::move(key)] = value;
1556 invalidate();
1557 return Result<void>::success();
1558}
1559
1560Result<void> MeshModifierGraph::setNodeInt(std::string_view id, std::string key, int value) {
1561 const auto found = nodes_.find(std::string(id));
1562 if (found == nodes_.end()) return Result<void>::failure(
1563 Diagnostic::error(DiagnosticCode::NotFound, "mesh modifier node not found", "node.id", {}, "procgen.meshModifierGraph"));
1564 const auto* spec = findSpec(found->second.operation);
1565 const auto* param = spec ? findParam(*spec, key) : nullptr;
1566 if (!param || std::string_view(param->kind) != "int")
1567 return Result<void>::failure(
1568 Diagnostic::error(DiagnosticCode::TypeMismatch, "unknown or non-integer mesh modifier parameter", key, {}, "procgen.meshModifierGraph"));
1569 found->second.ints[std::move(key)] = value;
1570 invalidate();
1571 return Result<void>::success();
1572}
1573
1574Result<void> MeshModifierGraph::setNodeString(std::string_view id, std::string key, std::string value) {
1575 const auto found = nodes_.find(std::string(id));
1576 if (found == nodes_.end()) return Result<void>::failure(
1577 Diagnostic::error(DiagnosticCode::NotFound, "mesh modifier node not found", "node.id", {}, "procgen.meshModifierGraph"));
1578 const auto* spec = findSpec(found->second.operation);
1579 const auto* param = spec ? findParam(*spec, key) : nullptr;
1580 if (!param || std::string_view(param->kind) != "string")
1581 return Result<void>::failure(
1582 Diagnostic::error(DiagnosticCode::TypeMismatch, "unknown or non-string mesh modifier parameter", key, {}, "procgen.meshModifierGraph"));
1583 found->second.strings[std::move(key)] = std::move(value);
1584 invalidate();
1585 return Result<void>::success();
1586}
1587
1588Result<void> MeshModifierGraph::validateNode(std::string_view id, std::unordered_map<std::string, int>& states) const {
1589 const std::string key(id);
1590 if (states[key] == 2) return Result<void>::success();
1591 if (states[key] == 1) return Result<void>::failure(
1592 Diagnostic::error(DiagnosticCode::Conflict, "cycle at mesh modifier node: " + key, key, {}, "procgen.meshModifierGraph"));
1593 const auto found = nodes_.find(key);
1594 if (found == nodes_.end()) return Result<void>::failure(
1595 Diagnostic::error(DiagnosticCode::NotFound, "unknown mesh modifier node: " + key, key, {}, "procgen.meshModifierGraph"));
1596 states[key] = 1;
1597 const Node& node = found->second;
1598 if (node.operation == "mesh.input") {
1599 if (!node.hasInputMesh)
1600 return Result<void>::failure(
1601 Diagnostic::error(DiagnosticCode::PreconditionViolation, "mesh.input has no bound mesh", key, {}, "procgen.meshModifierGraph"));
1602 auto valid = validateMesh(node.inputMesh, key);
1603 if (!valid.ok()) return valid;
1604 }
1605 for (std::size_t input = 0; input < node.inputs.size(); ++input) {
1606 if (node.inputs[input].empty())
1607 return Result<void>::failure(
1608 Diagnostic::error(DiagnosticCode::PreconditionViolation, node.operation + " requires mesh input " + std::to_string(input), key, {}, "procgen.meshModifierGraph"));
1609 auto valid = validateNode(node.inputs[input], states);
1610 if (!valid.ok()) return valid;
1611 }
1612 const std::string axis = stringParameter(node, "axis", "y");
1613 if ((node.operation == "deform.bend" || node.operation == "deform.twist" ||
1614 node.operation == "deform.angularBend" || node.operation == "deform.spline" ||
1615 node.operation == "deform.splinePath") &&
1616 axis != "x" && axis != "y" && axis != "z")
1617 return Result<void>::failure(
1618 Diagnostic::error(DiagnosticCode::InvalidArgument, "deformation axis must be x, y, or z", key + ".axis", {}, "procgen.meshModifierGraph"));
1619 if ((node.operation == "deform.splinePath" || node.operation == "mesh.splineTube" ||
1620 node.operation == "mesh.splineRibbon" || node.operation == "mesh.splineExtrude") &&
1621 !node.hasSplinePath)
1622 return Result<void>::failure(
1623 Diagnostic::error(DiagnosticCode::PreconditionViolation, node.operation + " has no bound spline path", key, {}, "procgen.meshModifierGraph"));
1624 if (node.hasSplinePath && node.splinePath.chunkCount() > 1) {
1625 if (node.operation == "deform.splinePath")
1626 return Result<void>::failure(
1627 Diagnostic::error(DiagnosticCode::PreconditionViolation, "deform.splinePath requires one connected chunk", key, {}, "procgen.meshModifierGraph"));
1628 for (int chunk = 0; chunk < node.splinePath.chunkCount(); ++chunk) {
1629 auto path = node.splinePath.chunkPathResult(chunk);
1630 if (!path.ok()) return Result<void>::failure(path.status());
1631 }
1632 }
1633 if (node.operation == "deform.splinePath" && std::abs(parameter(node, "scale", 1.f)) < 1e-7f)
1634 return Result<void>::failure(
1635 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline path scale must be non-zero", key + ".scale", {}, "procgen.meshModifierGraph"));
1636 if (node.operation == "mesh.splineTube") {
1637 const int pathSegments = intParameter(node, "pathSegments", 32);
1638 const int radialSegments = intParameter(node, "radialSegments", 12);
1639 if (parameter(node, "radius", 0.5f) <= 0.f)
1640 return Result<void>::failure(
1641 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline tube radius must be positive", key + ".radius", {}, "procgen.meshModifierGraph"));
1642 if (pathSegments < 1 || pathSegments > 4096)
1643 return Result<void>::failure(
1644 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline tube pathSegments must be in [1, 4096]", key + ".pathSegments", {}, "procgen.meshModifierGraph"));
1645 if (radialSegments < 3 || radialSegments > 256)
1646 return Result<void>::failure(
1647 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline tube radialSegments must be in [3, 256]", key + ".radialSegments", {}, "procgen.meshModifierGraph"));
1648 const std::uint64_t capVertices = node.splinePath.isClosed() || intParameter(node, "cap", 1) == 0
1649 ? 0u
1650 : 2u * static_cast<std::uint64_t>(radialSegments + 2);
1651 const std::uint64_t vertexCount =
1652 static_cast<std::uint64_t>(pathSegments + 1) * static_cast<std::uint64_t>(radialSegments + 1) + capVertices;
1653 if (vertexCount > 1'000'000u)
1654 return Result<void>::failure(
1655 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline tube exceeds the one-million vertex budget", key, {}, "procgen.meshModifierGraph"));
1656 }
1657 if (node.operation == "mesh.splineRibbon") {
1658 const int pathSegments = intParameter(node, "pathSegments", 32);
1659 if (parameter(node, "width", 2.f) <= 0.f)
1660 return Result<void>::failure(
1661 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline ribbon width must be positive", key + ".width", {}, "procgen.meshModifierGraph"));
1662 if (parameter(node, "thickness", 0.f) < 0.f)
1663 return Result<void>::failure(
1664 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline ribbon thickness must be non-negative", key + ".thickness", {}, "procgen.meshModifierGraph"));
1665 if (pathSegments < 1 || pathSegments > 4096)
1666 return Result<void>::failure(
1667 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline ribbon pathSegments must be in [1, 4096]", key + ".pathSegments", {}, "procgen.meshModifierGraph"));
1668 }
1669 if (node.operation == "mesh.splineExtrude") {
1670 if (!node.hasSplineProfile)
1671 return Result<void>::failure(
1672 Diagnostic::error(DiagnosticCode::PreconditionViolation, "mesh.splineExtrude has no bound profile", key, {}, "procgen.meshModifierGraph"));
1673 const int pathSegments = intParameter(node, "pathSegments", 32);
1674 if (pathSegments < 1 || pathSegments > 4096)
1675 return Result<void>::failure(
1676 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline extrusion pathSegments must be in [1, 4096]", key + ".pathSegments", {}, "procgen.meshModifierGraph"));
1677 const std::uint64_t stride = node.splineProfile.points.size() + (node.splineProfile.closed ? 1u : 0u);
1678 if (static_cast<std::uint64_t>(pathSegments + 1) * stride > 1'000'000u)
1679 return Result<void>::failure(
1680 Diagnostic::error(DiagnosticCode::InvalidArgument, "spline extrusion exceeds the one-million vertex budget", key, {}, "procgen.meshModifierGraph"));
1681 }
1682 if (node.operation == "deform.transform" &&
1683 (std::abs(parameter(node, "scaleX", 1.f)) < 1e-7f || std::abs(parameter(node, "scaleY", 1.f)) < 1e-7f ||
1684 std::abs(parameter(node, "scaleZ", 1.f)) < 1e-7f))
1685 return Result<void>::failure(
1686 Diagnostic::error(DiagnosticCode::InvalidArgument, "deformation scale components must be non-zero", key, {}, "procgen.meshModifierGraph"));
1687 if (node.operation == "deform.soundReact") {
1688 const float level = parameter(node, "level", 0.f), threshold = parameter(node, "threshold", 0.f);
1689 const std::string axis = stringParameter(node, "axis", "y");
1690 if (level < 0.f || level > 1.f)
1691 return Result<void>::failure(
1692 Diagnostic::error(DiagnosticCode::InvalidArgument, "sound-react level must be in [0, 1]", key + ".level", {}, "procgen.meshModifierGraph"));
1693 if (threshold < 0.f || threshold > 1.f)
1694 return Result<void>::failure(
1695 Diagnostic::error(DiagnosticCode::InvalidArgument, "sound-react threshold must be in [0, 1]", key + ".threshold", {}, "procgen.meshModifierGraph"));
1696 if (axis != "x" && axis != "y" && axis != "z")
1697 return Result<void>::failure(
1698 Diagnostic::error(DiagnosticCode::InvalidArgument, "sound-react axis must be x, y, or z", key + ".axis", {}, "procgen.meshModifierGraph"));
1699 if (parameter(node, "frequency", 1.f) < 0.f)
1700 return Result<void>::failure(
1701 Diagnostic::error(DiagnosticCode::InvalidArgument, "sound-react frequency must be non-negative", key + ".frequency", {}, "procgen.meshModifierGraph"));
1702 }
1703 if (node.operation == "deform.effector") {
1704 const int pointCount = intParameter(node, "pointCount", 1);
1705 if (pointCount < 1 || pointCount > 4)
1706 return Result<void>::failure(
1707 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh effector pointCount must be in [1, 4]", key + ".pointCount", {}, "procgen.meshModifierGraph"));
1708 if (parameter(node, "density", 1.f) <= 0.f)
1709 return Result<void>::failure(
1710 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh effector density must be positive", key + ".density", {}, "procgen.meshModifierGraph"));
1711 for (int point = 0; point < pointCount; ++point) {
1712 const std::string radius = "p" + std::to_string(point) + "radius";
1713 if (parameter(node, radius, 1.f) <= 0.f)
1714 return Result<void>::failure(
1715 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh effector radius must be positive", key + "." + radius, {}, "procgen.meshModifierGraph"));
1716 }
1717 }
1718 if (node.operation == "deform.meshFit") {
1719 const float x = parameter(node, "directionX", 0.f), y = parameter(node, "directionY", -1.f);
1720 const float z = parameter(node, "directionZ", 0.f);
1721 if (x * x + y * y + z * z < 1e-12f)
1722 return Result<void>::failure(
1723 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh fit direction must be non-zero", key + ".direction", {}, "procgen.meshModifierGraph"));
1724 if (parameter(node, "maxDistance", 10.f) <= 0.f)
1725 return Result<void>::failure(
1726 Diagnostic::error(DiagnosticCode::InvalidArgument, "mesh fit maxDistance must be positive", key + ".maxDistance", {}, "procgen.meshModifierGraph"));
1727 }
1728 states[key] = 2;
1729 return Result<void>::success();
1730}
1731
1733 std::unordered_map<std::string, int> states;
1734 for (const auto& id : nodeOrder_) {
1735 auto valid = validateNode(id, states);
1736 if (!valid.ok()) return valid;
1737 }
1738 return Result<void>::success();
1739}
1740
1741Result<std::vector<MeshModifierGraph::Segment>> MeshModifierGraph::compilePlan(std::string_view outputId) const {
1742 std::vector<std::string> order;
1743 std::unordered_map<std::string, int> states;
1744 std::function<Result<void>(const std::string&)> visit = [&](const std::string& id) -> Result<void> {
1745 if (states[id] == 2) return Result<void>::success();
1746 if (states[id] == 1) return Result<void>::failure(
1747 Diagnostic::error(DiagnosticCode::Conflict, "cycle at mesh modifier node: " + id, id, {}, "procgen.meshModifierGraph"));
1748 const auto found = nodes_.find(id);
1749 if (found == nodes_.end())
1750 return Result<void>::failure(
1751 Diagnostic::error(DiagnosticCode::NotFound, "unknown mesh modifier output: " + id, id, {}, "procgen.meshModifierGraph"));
1752 states[id] = 1;
1753 for (const auto& input : found->second.inputs) {
1754 if (input.empty())
1755 return Result<void>::failure(
1756 Diagnostic::error(DiagnosticCode::PreconditionViolation, found->second.operation + " has an unconnected input", id, {}, "procgen.meshModifierGraph"));
1757 auto result = visit(input);
1758 if (!result.ok()) return result;
1759 }
1760 states[id] = 2;
1761 order.push_back(id);
1762 return Result<void>::success();
1763 };
1764 auto visited = visit(std::string(outputId));
1765 if (!visited.ok()) return Result<std::vector<Segment>>::failure(visited.status());
1766 std::unordered_map<std::string, std::vector<std::string>> consumers;
1767 for (const auto& id : order)
1768 for (const auto& input : nodes_.at(id).inputs)
1769 if (!input.empty()) consumers[input].push_back(id);
1770 std::unordered_set<std::string> assigned;
1771 std::vector<Segment> segments;
1772 for (const auto& id : order) {
1773 if (assigned.contains(id)) continue;
1774 Segment segment;
1775 segment.nodes.push_back(id);
1776 assigned.insert(id);
1777 const auto* firstSpec = findSpec(nodes_.at(id).operation);
1778 std::string cursor = id;
1779 while (firstSpec && firstSpec->perVertex && consumers[cursor].size() == 1u) {
1780 const auto& next = consumers[cursor].front();
1781 const auto* nextSpec = findSpec(nodes_.at(next).operation);
1782 if (!nextSpec || !nextSpec->perVertex || assigned.contains(next)) break;
1783 segment.nodes.push_back(next);
1784 assigned.insert(next);
1785 cursor = next;
1786 }
1787 segment.fusedVertexTraversal = segment.nodes.size() > 1u;
1788 segments.push_back(std::move(segment));
1789 }
1790 return Result<std::vector<Segment>>::success(std::move(segments));
1791}
1792
1793Result<MeshBuild> MeshModifierGraph::executeFused(const Segment& segment,
1794 const std::unordered_map<std::string, MeshBuild>& outputs) const {
1795 const Node& first = nodes_.at(segment.nodes.front());
1796 const auto source = outputs.find(first.inputs.front());
1797 if (source == outputs.end())
1799 "fused mesh input was not evaluated", {}, {},
1800 "procgen.meshModifierGraph"));
1801 MeshBuild output = source->second;
1802 auto& positions = output.positions();
1803 auto& normals = output.normals();
1804 for (int vertex = 0; vertex < output.getVertexCount(); ++vertex) {
1805 const std::size_t p = std::size_t(vertex) * 3u;
1806 Vec3 position{positions[p], positions[p + 1], positions[p + 2]};
1807 Vec3 normal{normals[p], normals[p + 1], normals[p + 2]};
1808 for (const auto& id : segment.nodes) applyMaskedVertexOperation(nodes_.at(id), position, normal);
1809 positions[p] = position.x;
1810 positions[p + 1] = position.y;
1811 positions[p + 2] = position.z;
1812 normals[p] = normal.x;
1813 normals[p + 1] = normal.y;
1814 normals[p + 2] = normal.z;
1815 }
1816 return Result<MeshBuild>::success(std::move(output));
1817}
1818
1819Result<MeshBuild> MeshModifierGraph::executeNode(const Node& node,
1820 const std::unordered_map<std::string, MeshBuild>& outputs) const {
1821 if (node.operation == "mesh.input") return Result<MeshBuild>::success(node.inputMesh);
1822 if (node.operation == "mesh.splineTube") return splineTubeMesh(node);
1823 if (node.operation == "mesh.splineRibbon") return splineRibbonMesh(node);
1824 if (node.operation == "mesh.splineExtrude") return splineExtrudeMesh(node);
1825 const auto first = outputs.find(node.inputs.front());
1826 if (first == outputs.end())
1828 "mesh node input was not evaluated", node.id, {},
1829 "procgen.meshModifierGraph"));
1830 if (const auto* spec = findSpec(node.operation); spec && spec->perVertex) {
1831 MeshBuild output = first->second;
1832 auto& positions = output.positions();
1833 auto& normals = output.normals();
1834 for (int vertex = 0; vertex < output.getVertexCount(); ++vertex) {
1835 const std::size_t p = std::size_t(vertex) * 3u;
1836 Vec3 position{positions[p], positions[p + 1], positions[p + 2]};
1837 Vec3 normal{normals[p], normals[p + 1], normals[p + 2]};
1838 applyMaskedVertexOperation(node, position, normal);
1839 positions[p] = position.x;
1840 positions[p + 1] = position.y;
1841 positions[p + 2] = position.z;
1842 normals[p] = normal.x;
1843 normals[p + 1] = normal.y;
1844 normals[p + 2] = normal.z;
1845 }
1846 return Result<MeshBuild>::success(std::move(output));
1847 }
1848 if (node.operation == "mesh.output") return Result<MeshBuild>::success(first->second);
1849 if (node.operation == "deform.angularBend") return angularBendMesh(first->second, node);
1850 if (node.operation == "deform.ffd") return ffdMesh(first->second, node);
1851 if (node.operation == "deform.spline") return splineMesh(first->second, node);
1852 if (node.operation == "deform.splinePath") return splinePathMesh(first->second, node);
1853 if (node.operation == "deform.morph") {
1854 const auto second = outputs.find(node.inputs[1]);
1855 if (second == outputs.end())
1857 "deform.morph second input was not evaluated", node.id,
1858 {}, "procgen.meshModifierGraph"));
1859 return morphMesh(first->second, second->second, parameter(node, "weight", 0.5f));
1860 }
1861 if (node.operation == "deform.meshFit") {
1862 const auto second = outputs.find(node.inputs[1]);
1863 if (second == outputs.end())
1865 "deform.meshFit surface input was not evaluated",
1866 node.id, {}, "procgen.meshModifierGraph"));
1867 return meshFitMesh(first->second, second->second, node);
1868 }
1869 if (node.operation == "deform.meshAdhere") {
1870 const auto second = outputs.find(node.inputs[1]);
1871 if (second == outputs.end())
1873 "deform.meshAdhere surface input was not evaluated",
1874 node.id, {}, "procgen.meshModifierGraph"));
1875 MeshContactBlendParams params;
1876 const std::string falloff = stringParameter(node, "falloff", "smooth");
1877 params.strength = parameter(node, "strength", 1.f);
1878 params.edgeRadius = parameter(node, "edgeRadius", 0.5f);
1879 params.materialRadius = parameter(node, "materialRadius", 0.5f);
1880 params.normalsBlend = parameter(node, "normalsBlend", 1.f);
1881 params.materialBlend = parameter(node, "materialBlend", 1.f);
1882 params.surfaceOffset = parameter(node, "surfaceOffset", 0.f);
1883 params.maxQueryDistance = parameter(node, "maxQueryDistance", 0.f);
1884 params.softSnapPositions = intParameter(node, "softSnapPositions", 1) != 0;
1885 params.falloff = falloff;
1886 auto adhered = meshContactBlendAgainstSurfaceResult(first->second, second->second, params);
1887 if (!adhered.ok()) return adhered;
1888 MeshBuild output = std::move(adhered).takeValue();
1889 output.setMeta("deformer", "deform.meshAdhere");
1890 return Result<MeshBuild>::success(std::move(output));
1891 }
1892 if (node.operation == "deform.smooth")
1893 return smoothMesh(first->second, parameter(node, "strength", 0.5f), intParameter(node, "iterations", 1));
1894 if (node.operation == "mesh.subdivide") return subdivideMesh(first->second, intParameter(node, "levels", 1));
1895 if (node.operation == "mesh.cutPlane") return cutPlaneMesh(first->second, node);
1896 if (node.operation == "mesh.append") {
1897 const auto second = outputs.find(node.inputs[1]);
1898 if (second == outputs.end())
1900 "mesh.append second input was not evaluated", node.id,
1901 {}, "procgen.meshModifierGraph"));
1902 MeshBuild output = first->second;
1903 if (!output.appendTransformed(&second->second, 0.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f))
1905 "mesh.append rejected an empty or invalid input",
1906 node.id, {}, "procgen.meshModifierGraph"));
1907 return Result<MeshBuild>::success(std::move(output));
1908 }
1909 if (node.operation == "mesh.boolean") {
1910 const auto second = outputs.find(node.inputs[1]);
1911 if (second == outputs.end())
1913 "mesh.boolean second input was not evaluated", node.id,
1914 {}, "procgen.meshModifierGraph"));
1915 return meshBooleanResult(first->second, second->second,
1916 stringParameter(node, "operation", "difference"));
1917 }
1918 if (node.operation == "mesh.projectUv")
1919 return projectMeshUvResult(first->second, stringParameter(node, "mode", "box"),
1920 parameter(node, "scale", 1.f), parameter(node, "offsetU", 0.f),
1921 parameter(node, "offsetV", 0.f));
1922 if (node.operation == "mesh.weld") return weldMesh(first->second, parameter(node, "tolerance", 0.0001f));
1924 "unsupported mesh modifier operation: " + node.operation,
1925 node.id, {}, "procgen.meshModifierGraph"));
1926}
1927
1929 const auto requested = nodes_.find(std::string(outputId));
1930 if (requested == nodes_.end())
1932 "unknown mesh modifier output: " + std::string(outputId),
1933 std::string(outputId), {}, "procgen.meshModifierGraph"));
1934 if (requested->second.cacheValid) {
1935 metrics_ = {{requested->first, requested->second.cache.getVertexCount(),
1936 requested->second.cache.getIndexCount() / 3, 0.f, true, false}};
1937 compiledSegmentCount_ = 0;
1938 fusedOperationCount_ = 0;
1939 return Result<MeshBuild>::success(requested->second.cache);
1940 }
1941 std::unordered_map<std::string, int> validationStates;
1942 auto valid = validateNode(outputId, validationStates);
1943 if (!valid.ok()) return Result<MeshBuild>::failure(valid.status());
1944 auto planned = compilePlan(outputId);
1945 if (!planned.ok()) return Result<MeshBuild>::failure(planned.status());
1946 const auto plan = std::move(planned).takeValue();
1947 std::unordered_map<std::string, MeshBuild> outputs;
1948 std::vector<MeshModifierNodeMetric> candidateMetrics;
1949 int candidateFused = 0;
1950 for (const auto& segment : plan) {
1951 const auto started = std::chrono::steady_clock::now();
1952 Result<MeshBuild> evaluated = segment.fusedVertexTraversal
1953 ? executeFused(segment, outputs)
1954 : executeNode(nodes_.at(segment.nodes.front()), outputs);
1955 if (!evaluated.ok()) return Result<MeshBuild>::failure(evaluated.status());
1956 MeshBuild output = std::move(evaluated).takeValue();
1957 const auto elapsed =
1958 std::chrono::duration<float, std::milli>(std::chrono::steady_clock::now() - started).count();
1959 const std::string& outputNode = segment.nodes.back();
1960 candidateMetrics.push_back({outputNode, output.getVertexCount(), output.getIndexCount() / 3, elapsed, false,
1961 segment.fusedVertexTraversal});
1962 if (segment.fusedVertexTraversal) candidateFused += static_cast<int>(segment.nodes.size());
1963 outputs.emplace(outputNode, std::move(output));
1964 }
1965 const auto found = outputs.find(std::string(outputId));
1966 if (found == outputs.end())
1968 "mesh modifier output was not produced",
1969 std::string(outputId), {}, "procgen.meshModifierGraph"));
1970 for (auto& [id, node] : nodes_) {
1971 const auto output = outputs.find(id);
1972 if (output != outputs.end()) {
1973 node.cache = output->second;
1974 node.cacheValid = true;
1975 }
1976 }
1977 metrics_ = std::move(candidateMetrics);
1978 compiledSegmentCount_ = static_cast<int>(plan.size());
1979 fusedOperationCount_ = candidateFused;
1980 ++executionPlanBuildCount_;
1981 return Result<MeshBuild>::success(found->second);
1982}
1983
1985 for (auto& [_, node] : nodes_) {
1986 node.cache.clear();
1987 node.cacheValid = false;
1988 }
1989 metrics_.clear();
1990 compiledSegmentCount_ = 0;
1991 fusedOperationCount_ = 0;
1992}
1993
1994void MeshModifierGraph::invalidate() {
1995 clearCache();
1996 ++revision_;
1997}
1998
1999bool MeshModifierGraph::hasNode(std::string_view id) const { return nodes_.contains(std::string(id)); }
2000
2001std::string MeshModifierGraph::nodeId(int index) const {
2002 return index >= 0 && index < static_cast<int>(nodeOrder_.size()) ? nodeOrder_[static_cast<std::size_t>(index)]
2003 : std::string();
2004}
2005
2006std::string MeshModifierGraph::nodeOperation(std::string_view id) const {
2007 const auto found = nodes_.find(std::string(id));
2008 return found == nodes_.end() ? std::string() : found->second.operation;
2009}
2010
2011int MeshModifierGraph::operationCount() { return static_cast<int>(operationSpecs().size()); }
2013 return index >= 0 && index < operationCount() ? operationSpecs()[static_cast<std::size_t>(index)].id
2014 : std::string();
2015}
2017 const auto* spec = findSpec(operation);
2018 return spec ? spec->inputs : -1;
2019}
2021 const auto* spec = findSpec(operation);
2022 return spec ? static_cast<int>(spec->params.size()) : 0;
2023}
2024std::string MeshModifierGraph::operationParamKey(std::string_view operation, int index) {
2025 const auto* spec = findSpec(operation);
2026 return spec && index >= 0 && index < static_cast<int>(spec->params.size())
2027 ? spec->params[static_cast<std::size_t>(index)].key
2028 : std::string();
2029}
2030std::string MeshModifierGraph::operationParamKind(std::string_view operation, int index) {
2031 const auto* spec = findSpec(operation);
2032 return spec && index >= 0 && index < static_cast<int>(spec->params.size())
2033 ? spec->params[static_cast<std::size_t>(index)].kind
2034 : std::string();
2035}
2037 const auto* spec = findSpec(operation);
2038 return spec && index >= 0 && index < static_cast<int>(spec->params.size())
2039 ? spec->params[static_cast<std::size_t>(index)].defaultValue
2040 : std::string();
2041}
2042
2043} // namespace eve::procgen
LogicalId target
ActionParameterOperation operation
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::string output
const std::string & s
float phase
Definition CaveMesh.cpp:58
Vec3 tangent
Definition CaveMesh.cpp:80
float length
Definition CaveMesh.cpp:94
float thickness
Definition CaveMesh.cpp:83
float nx
float ny
glm::vec4 p[6]
std::map< std::string, Var > values
EvpackChunkInput input
Definition Evpack.cpp:170
int triangle
std::uint32_t vertexCount
float maximum[3]
float minimum[3]
std::uint32_t key
float u
Definition Grass.cpp:233
float area
Definition Grass.cpp:62
int inputs
Definition GridGraph.cpp:23
std::array< double, 10 > q
std::uint32_t ab
double r
std::uint32_t ac
std::vector< std::uint32_t > indices
std::vector< float > normals
std::vector< float > positions
float v
HexVec3 up
HexVec3 left
HexVec3 right
std::int32_t second
std::int32_t c
std::int32_t first
float blend
std::uint32_t width
TokenKind kind
size_t offset
std::array< float, 3 > position
std::array< float, 3 > scale
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
Vec3 centroid
std::vector< std::int32_t > order
std::vector< BvhNode > nodes
std::vector< TriangleRef > triangles
Texture * normal
int level
graphics::Canvas * previous
std::string error
Definition Package.cpp:60
std::vector< Point > vertices
float radius
float halfWidth
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::uint32_t seed
Definition PointSet.cpp:807
std::shared_ptr< const std::vector< glm::vec2 > > points
float t
Mesh * mesh
std::unordered_map< std::uint32_t, std::uint32_t > remap
std::vector< float > colors
V3 origin
Definition RoadBake.cpp:138
const RoadNode * node
RoadLaneDirection direction
const RoadEdge * edge
bool found
double current
std::string string
float dz
float dy
float dx
std::uint32_t count
std::map< Cell, int > best
std::size_t cursor
ecs::EntityHandle side
int iterations
Definition TreeMesh.cpp:311
float size
Definition TreeMesh.cpp:156
uint32_t index
const UnitySourceAsset & source
std::size_t at
std::vector< int > edges
glm::vec3 point
float angle
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
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:255
const Status & status() const noexcept
Inspect the structured operation status.
Definition Result.h:269
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
CPU triangle mesh from procedural mesh recipes (e.g. marching cubes). Positions/normals are xyz-packe...
Definition MeshBuild.h:19
std::string nodeOperation(std::string_view id) const
Return a node operation, or an empty string when missing.
Result< void > setNodeSplinePath(std::string_view id, const SplinePath &path)
Copy an owning path snapshot into a spline deformation or generation node.
bool hasNode(std::string_view id) const
Report whether a node exists.
Result< void > setNodeInt(std::string_view id, std::string key, int value)
Set an integer node parameter.
std::string nodeId(int index) const
Return a node id by insertion order, or an empty string when out of range.
Result< void > setNodeSplineProfile(std::string_view id, const SplineProfile &profile)
Copy an owning cross-section snapshot into a spline extrusion node.
static std::string operationParamDefault(std::string_view operation, int index)
Return encoded reflected parameter default.
static int operationParamCount(std::string_view operation)
Return reflected parameter count.
static std::string operationParamKind(std::string_view operation, int index)
Return reflected parameter kind: float, int, or string.
Result< void > validateResult() const
Validate topology, inputs, parameters, and mesh stream invariants.
Result< void > removeNode(std::string_view id)
Remove a node and all incoming edges that reference it.
Result< void > disconnect(std::string_view toId, int inputIndex=0)
Disconnect a numbered mesh input.
void clearCache()
Clear all derived outputs without changing graph topology or inputs.
Result< void > connect(std::string_view fromId, std::string_view toId, int inputIndex=0)
Connect one node output to a numbered mesh input.
static std::string operationParamKey(std::string_view operation, int index)
Return reflected parameter key.
Result< void > addNode(std::string id, std::string operation)
Add a node from the reflected operation catalogue.
static int operationInputCount(std::string_view operation)
Return required mesh input count, or -1 for an unknown operation.
Result< void > setNodeString(std::string_view id, std::string key, std::string value)
Set a string node parameter.
static int operationCount()
Return reflected operation count.
Result< void > setNodeFloat(std::string_view id, std::string key, float value)
Set a finite floating-point node parameter.
static std::string operationId(int index)
Return reflected operation id, or an empty string for an invalid index.
Result< MeshBuild > executeResult(std::string_view outputId)
Execute one output and return an independent owning mesh snapshot.
Result< void > setNodeMesh(std::string_view id, const MeshBuild &mesh)
Copy an owning mesh snapshot into an input node.
Owning multi-segment 3D spline with deterministic bounded sampling.
Definition SplinePath.h:91
const char * defaultValue
std::vector< ParamSpec > params
bool perVertex
Vec3 interpolate(Vec3 start, Vec3 end, float amount) noexcept
Interpolate.
eve::Diagnostic Diagnostic
eve::Result< T > Result
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
double sample(const Heightmap &map, double u, double v)
Sample.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
double cross(const Vec2 &a, const Vec2 &b)
Cross.
Definition UrbanTypes.h:36
Result< MeshBuild > meshBooleanResult(const MeshBuild &left, const MeshBuild &right, std::string_view operation)
Evaluate a closed-triangle solid boolean using a BSP polygon split.
Result< MeshBuild > meshContactBlendAgainstSurfaceResult(const MeshBuild &movable, const MeshBuild &surface, const MeshContactBlendParams &params)
Deform movable toward surface only (dynamic adhere / one-way fusion).
void rebuildSoftSnapContactNormals(MeshBuild &mesh)
Rebuild triangle normals then 1-ring-average them (soft-snap / post-weld lighting).
Result< MeshBuild > projectMeshUvResult(const MeshBuild &input, std::string_view mode, float scale, float offsetU, float offsetV)
Generate topology-preserving UVs for a procedural or deformed mesh.
int axis(int64_t a, size_t rank)
Axis.
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
@ TypeMismatch
A stable reference resolved to a different canonical domain type.
Owning cross-section snapshot used by mesh.splineExtrude.
std::vector< SplineProfilePoint > points
bool started
Definition Graphics.cpp:183