14#include <unordered_set>
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, {}},
40 {{
"radius",
"float",
"0.5"},
41 {
"pathSegments",
"int",
"32"},
42 {
"radialSegments",
"int",
"12"},
43 {
"roll",
"float",
"0"},
44 {
"cap",
"int",
"1"}}},
48 {{
"width",
"float",
"2"},
49 {
"thickness",
"float",
"0"},
50 {
"pathSegments",
"int",
"32"},
51 {
"roll",
"float",
"0"},
52 {
"cap",
"int",
"1"}}},
53 {
"mesh.splineExtrude",
56 {{
"pathSegments",
"int",
"32"}, {
"roll",
"float",
"0"}, {
"cap",
"int",
"1"}}},
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"}}},
74 {{
"amplitude",
"float",
"0.1"}, {
"frequency",
"float",
"1"}, {
"seed",
"int",
"1"}}},
78 {{
"level",
"float",
"0"},
79 {
"threshold",
"float",
"0"},
80 {
"strength",
"float",
"1"},
81 {
"frequency",
"float",
"1"},
82 {
"phase",
"float",
"0"},
83 {
"axis",
"string",
"y"}}},
90 {
"radius",
"float",
"1"},
91 {
"strength",
"float",
"0.1"},
92 {
"falloff",
"float",
"1"}}},
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",
111 {{
"angle",
"float",
"0"}, {
"direction",
"float",
"0"}, {
"axis",
"string",
"y"}}},
115 {{
"x",
"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"}}},
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",
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"}}},
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",
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"}}},
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"}}},
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"}});
203const OperationSpec* findSpec(std::string_view
operation) {
204 const auto& specs = operationSpecs();
206 std::find_if(specs.begin(), specs.end(), [&](
const OperationSpec& spec) { return operation == spec.id; });
207 return found == specs.end() ? nullptr : &*
found;
210const ParamSpec* findParam(
const OperationSpec& spec, std::string_view
key) {
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;
216float parameter(
const auto&
node, std::string_view
key,
float fallback) {
221int intParameter(
const auto&
node, std::string_view
key,
int fallback) {
226std::string stringParameter(
const auto&
node, std::string_view
key, std::string fallback) {
232 float x = 0.f,
y = 0.f,
z = 0.f;
236 const float c = std::cos(radians),
s = std::sin(radians);
241 const float c = std::cos(radians),
s = std::sin(radians);
246 const float c = std::cos(radians),
s = std::sin(radians);
250float coherentNoise(Vec3
value,
int seed) {
251 const float phase =
static_cast<float>(
seed);
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);
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);
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);
278 const float angle = parameter(
node,
"angle", 0.f) * radiansPerDegree * along / extent;
279 if (
node.operation ==
"deform.twist") {
283 }
else if (axis ==
"z") {
294 }
else if (axis ==
"z") {
302 }
else if (
node.operation ==
"deform.noise") {
303 const float frequency = parameter(
node,
"frequency", 1.f);
305 const int seed = intParameter(
node,
"seed", 1);
306 const float amplitude = parameter(
node,
"amplitude", 0.1f);
310 }
else if (
node.operation ==
"deform.soundReact") {
311 const std::string
axis = stringParameter(
node,
"axis",
"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));
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;
329 }
else if (
node.operation ==
"deform.radial") {
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);
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);
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)};
356 const float radius = parameter(
node, prefix +
"radius", 1.f);
358 const float influence =
359 std::pow(1.f -
distance /
radius, density) * parameter(
node, prefix +
"weight", 1.f) * multiplier;
364 }
else if (
node.operation ==
"deform.spherify") {
367 const float length = std::sqrt(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
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);
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);
393 std::pow(std::clamp(1.f -
distance /
radius, 0.f, 1.f), std::max(parameter(
node,
"maskFalloff", 1.f), 0.01f));
403Result<void> validateMesh(
const MeshBuild&
mesh, std::string_view
path) {
406 if (
mesh.positions().size() % 3u != 0
u ||
mesh.normals().size() !=
mesh.positions().size() ||
407 mesh.uvs().size() != (
mesh.positions().size() / 3u) * 2u ||
mesh.indices().size() % 3u != 0
u)
411 if (
index >= static_cast<
std::uint32_t>(
mesh.getVertexCount()))
412 return
Result<void>::failure(
417void recalculateNormals(MeshBuild&
mesh) {
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;
431 for (
const auto vertex : {
a,
b,
c}) {
437 for (std::size_t i = 0; i + 2 <
normals.size(); i += 3) {
451 std::vector<std::unordered_set<std::uint32_t>> neighbors(
static_cast<std::size_t
>(
count));
453 for (std::size_t i = 0; i + 2 <
indices.size(); i += 3) {
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]);
463 for (
int iteration = 0; iteration <
iterations; ++iteration) {
465 for (
int vertex = 0; vertex <
count; ++vertex) {
466 const auto& adjacent = neighbors[
static_cast<std::size_t
>(vertex)];
467 if (adjacent.empty())
continue;
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];
474 const float divisor = float(adjacent.size());
475 const std::size_t base = std::size_t(vertex) * 3u;
487void copyMetadata(
const MeshBuild&
input, MeshBuild&
output) {
491Result<MeshBuild> angularBendMesh(
const MeshBuild&
input,
const auto&
node) {
495 const std::string
axis = stringParameter(
node,
"axis",
"y");
496 const int component =
axis ==
"x" ? 0 :
axis ==
"z" ? 2 : 1;
499 for (std::size_t i =
static_cast<std::size_t
>(component); i <
positions.size(); i += 3u) {
504 const float direction = parameter(
node,
"direction", 0.f) * 0.01745329251994329577f;
506 const float angle = parameter(
node,
"angle", 0.f) * 0.01745329251994329577f;
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);
533Result<MeshBuild> splineMesh(
const MeshBuild&
input,
const auto&
node) {
537 const std::string
axis = stringParameter(
node,
"axis",
"y");
538 const int component =
axis ==
"x" ? 0 :
axis ==
"z" ? 2 : 1;
541 for (std::size_t i =
static_cast<std::size_t
>(component); i <
positions.size(); i += 3u) {
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)};
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};
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)];
564 for (std::size_t base = 0; base <
positions.size(); base += 3u) {
566 const float t = std::clamp((component == 0 ?
source.x
577 recalculateNormals(
output);
581Result<MeshBuild> splinePathMesh(
const MeshBuild&
input,
const auto&
node) {
585 const std::string
axis = stringParameter(
node,
"axis",
"y");
586 const int component =
axis ==
"x" ? 0 :
axis ==
"z" ? 2 : 1;
589 for (std::size_t i =
static_cast<std::size_t
>(component); i <
positions.size(); i += 3u) {
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) {
600 auto sampled =
node.splinePath.evaluateResult((along -
minimum) / extent);
602 const auto&
sample = sampled.value();
606 side = {0.f, -1.f, 0.f};
607 up = {-1.f, 0.f, 0.f};
609 const float a = 1.f / (1.f +
tangent.z);
614 const float c = std::cos(
roll),
s = std::sin(
roll);
617 const float localA = component == 0 ?
source.y :
source.x;
618 const float localB = component == 2 ?
source.y :
source.z;
620 sample.y + (rolledSide.y * localA + rolledUp.y * localB) *
scale,
621 sample.z + (rolledSide.z * localA + rolledUp.z * localB) *
scale};
626 recalculateNormals(
output);
630Result<MeshBuild> splineTubeMesh(
const auto&
node) {
631 if (
node.splinePath.chunkCount() > 1) {
633 for (
int chunk = 0; chunk <
node.splinePath.chunkCount(); ++chunk) {
634 auto path =
node.splinePath.chunkPathResult(chunk);
636 auto chunkNode =
node;
637 chunkNode.splinePath = std::move(
path).takeValue();
638 auto mesh = splineTubeMesh(chunkNode);
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"));
645 output.setMeta(
"generator",
"mesh.splineTube");
646 output.setMeta(
"chunks", std::to_string(
node.splinePath.chunkCount()));
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;
657 output.reserve(ringCount * ringStride + (capped ? 2 * (ringStride + 1) : 0),
658 pathSegments * radialSegments * 6 + (capped ? radialSegments * 6 : 0));
660 constexpr float tau = 6.28318530717958647692f;
661 auto sampledFrames =
node.splinePath.sampleFramesResult(pathSegments,
true,
roll, 24);
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;
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};
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);
698 output.setActiveGroup(
"caps");
699 auto startSample =
node.splinePath.evaluateResult(0.f);
700 auto endSample =
node.splinePath.evaluateResult(1.f);
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;
713 startNormal.x, startNormal.y, startNormal.z, 0.5f + std::cos(
angle) * 0.5f,
714 0.5f + std::sin(
angle) * 0.5f);
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);
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);
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");
741Result<MeshBuild> splineRibbonMesh(
const auto&
node) {
742 if (
node.splinePath.chunkCount() > 1) {
744 for (
int chunk = 0; chunk <
node.splinePath.chunkCount(); ++chunk) {
745 auto path =
node.splinePath.chunkPathResult(chunk);
747 auto chunkNode =
node;
748 chunkNode.splinePath = std::move(
path).takeValue();
749 auto mesh = splineRibbonMesh(chunkNode);
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"));
756 output.setMeta(
"generator",
"mesh.splineRibbon");
757 output.setMeta(
"chunks", std::to_string(
node.splinePath.chunkCount()));
760 const float width = parameter(
node,
"width", 2.f);
762 const int pathSegments = intParameter(
node,
"pathSegments", 32);
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);
768 const int ringStride = solid ? 8 : 2;
770 output.reserve((pathSegments + 1) * ringStride + (capped ? 8 : 0),
771 pathSegments * (solid ? 24 : 6) + (capped ? 12 : 0));
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;
785 if (!solid)
continue;
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);
794 output.addVertex(bottomLeft.x, bottomLeft.y, bottomLeft.z, -
side.x, -
side.y, -
side.z,
u, 1.f);
796 output.addVertex(bottomRight.x, bottomRight.y, bottomRight.z,
side.x,
side.y,
side.z,
u, 1.f);
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);
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);
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);
824 output.setActiveGroup(
"caps");
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 : {0
u, 1u, 2u, 3u}) {
835 output.addVertex(
output.getPositionX(
static_cast<int>(vertex)),
836 output.getPositionY(
static_cast<int>(vertex)),
841 output.addTriangle(base, base + 1u, base + 3u);
842 output.addTriangle(base + 1u, base + 2u, base + 3u);
844 output.addTriangle(base, base + 3u, base + 1u);
845 output.addTriangle(base + 1u, base + 3u, base + 2u);
850 output.setMeta(
"generator",
"mesh.splineRibbon");
851 output.setMeta(
"pathSegments", std::to_string(pathSegments));
852 output.setMeta(
"closed",
node.splinePath.isClosed() ?
"1" :
"0");
856float profileArea(
const SplineProfile& profile) {
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()];
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);
870bool insideTriangle(
const SplineProfilePoint&
p,
const SplineProfilePoint&
a,
const SplineProfilePoint&
b,
871 const SplineProfilePoint&
c) {
872 constexpr float epsilon = 1e-7f;
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);
881 for (std::size_t i = 0; i < polygon.size(); ++i)
882 polygon[i] =
static_cast<std::uint32_t
>(polygon.size() - 1u - i);
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])) {
900 if (contains)
continue;
902 polygon.erase(polygon.begin() +
static_cast<std::ptrdiff_t
>(i));
909 "profile", {},
"procgen.meshModifierGraph"));
912 return Result<std::vector<std::uint32_t>>::success(std::move(
triangles));
915Result<MeshBuild> splineExtrudeMesh(
const auto&
node) {
916 if (
node.splinePath.chunkCount() > 1) {
918 for (
int chunk = 0; chunk <
node.splinePath.chunkCount(); ++chunk) {
919 auto path =
node.splinePath.chunkPathResult(chunk);
921 auto chunkNode =
node;
922 chunkNode.splinePath = std::move(
path).takeValue();
923 auto mesh = splineExtrudeMesh(chunkNode);
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"));
930 output.setMeta(
"generator",
"mesh.splineExtrude");
931 output.setMeta(
"chunks", std::to_string(
node.splinePath.chunkCount()));
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);
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);
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;
960 const float nl = std::sqrt(
nx *
nx +
ny *
ny);
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));
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);
981 auto triangles = triangulateProfile(profile);
983 output.setActiveGroup(
"caps");
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;
993 frame.sample.tangentZ *
direction, profile.points[
static_cast<std::size_t
>(i)].
x,
994 profile.points[
static_cast<std::size_t
>(i)].y);
996 for (std::size_t i = 0; i <
triangles.value().size(); i += 3u) {
998 const auto c = base +
triangles.value()[i + 2u];
1006 output.setMeta(
"generator",
"mesh.splineExtrude");
1007 output.setMeta(
"profileClosed", profile.closed ?
"1" :
"0");
1008 output.setMeta(
"closed",
node.splinePath.isClosed() ?
"1" :
"0");
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};
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;
1022 if (u < 0.f || u > 1.f)
return false;
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};
1032 if (normalLength < 1e-7f)
return false;
1037Result<MeshBuild> meshFitMesh(
const MeshBuild&
input,
const MeshBuild& surface,
const auto&
node) {
1041 parameter(
node,
"directionZ", 0.f)};
1042 const float 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;
1054 Vec3 bestDirection{}, bestNormal{};
1055 for (
const float sign : {1.f, -1.f}) {
1056 if (sign < 0.f && !bidirectional)
continue;
1059 const auto point = [&](std::uint32_t
index) {
1060 const std::size_t
target =
static_cast<std::size_t
>(
index) * 3u;
1070 bestDirection = ray;
1079 recalculateNormals(
output);
1080 output.setMeta(
"deformer",
"deform.meshFit");
1084Result<MeshBuild> ffdMesh(
const MeshBuild&
input,
const auto&
node) {
1090 for (std::size_t i = 0; i <
positions.size(); i += 3u) {
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)};
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) {
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);
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)];
1124 recalculateNormals(
output);
1128Result<MeshBuild> morphMesh(
const MeshBuild&
first,
const MeshBuild&
second,
float weight) {
1131 "deform.morph inputs must have identical topology",
1132 "deform.morph", {},
"procgen.meshModifierGraph"));
1135 for (std::size_t i = 0; i <
output.positions().
size(); ++i)
1137 recalculateNormals(
output);
1141Result<MeshBuild> subdivideMesh(
const MeshBuild&
input,
int levels) {
1143 levels = std::clamp(levels, 1, 4);
1147 std::vector<int> assignments;
1148 auto addSource = [&](std::uint32_t
index) {
1153 return static_cast<std::uint32_t
>(
next.getVertexCount() - 1);
1155 auto midpoint = [&](std::uint32_t
a, std::uint32_t
b) {
1164 return static_cast<std::uint32_t
>(
next.getVertexCount() - 1);
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]);
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);
1183 recalculateNormals(next);
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)};
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)};
1210 "mesh.cutPlane requires a non-zero normal",
"normal", {},
1211 "procgen.meshModifierGraph"));
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) {
1221 std::vector<int> assignments;
1222 std::vector<std::array<Vec3, 2>> cutSegments;
1224 std::vector<ClipVertex> polygon;
1225 for (
int corner = 0; corner < 3; ++corner) {
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) {
1242 clipped.push_back(intersection);
1243 intersections.push_back(intersection.position);
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)
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));
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) {
1268 if (
dx *
dx +
dy *
dy +
dz *
dz <= toleranceSquared)
return static_cast<int>(i);
1271 return static_cast<int>(
points.size() - 1u);
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);
1281 std::vector<bool> used(
edges.size(),
false);
1283 if (used[
seed])
continue;
1286 while (
loop.back() !=
loop.front()) {
1287 bool extended =
false;
1289 if (used[
edge])
continue;
1297 if (!extended ||
loop.size() >
edges.size() + 1u)
break;
1299 if (
loop.size() < 4u ||
loop.back() !=
loop.front())
continue;
1307 const float inverseCount = 1.f /
static_cast<float>(
loop.size());
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;
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());
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);
1335 auto restored =
output.restoreGroupData(std::move(names), std::move(assignments), -1);
1342 std::int64_t
x,
y,
z;
1343 bool operator==(
const WeldKey&)
const =
default;
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);
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"));
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)};
1370 remap[
static_cast<std::size_t
>(i)] =
found->second;
1373 const auto next =
static_cast<std::uint32_t
>(
output.getVertexCount());
1375 remap[
static_cast<std::size_t
>(i)] = next;
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;
1386 assignments.push_back(
input.getTriangleGroup(i / 3));
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);
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);
1416 if (nodes_.contains(
id))
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);
1429 node.strings[param.key] = param.defaultValue;
1431 nodes_.emplace(
id, std::move(
node));
1432 nodeOrder_.push_back(std::move(
id));
1438 const std::string
key(
id);
1439 if (!nodes_.erase(
key))
1442 std::erase(nodeOrder_,
key);
1443 for (
auto& [_,
node] : nodes_)
1451 const auto source = nodes_.find(std::string(fromId));
1452 const auto target = nodes_.find(std::string(toId));
1459 if (inputIndex < 0 || inputIndex >=
static_cast<int>(
target->second.inputs.size()))
1462 target->second.inputs[
static_cast<std::size_t
>(inputIndex)] =
source->first;
1468 const auto target = nodes_.find(std::string(toId));
1469 if (
target == nodes_.end())
1472 if (inputIndex < 0 || inputIndex >=
static_cast<int>(
target->second.inputs.size()))
1475 target->second.inputs[
static_cast<std::size_t
>(inputIndex)].clear();
1481 const auto found = nodes_.find(std::string(
id));
1484 if (
found->second.operation !=
"mesh.input")
1487 auto valid = validateMesh(
mesh,
"mesh");
1490 found->second.hasInputMesh =
true;
1496 const auto found = nodes_.find(std::string(
id));
1499 if (
found->second.operation !=
"deform.splinePath" &&
found->second.operation !=
"mesh.splineTube" &&
1500 found->second.operation !=
"mesh.splineRibbon" &&
found->second.operation !=
"mesh.splineExtrude")
1503 auto ready =
path.evaluateResult(0.f);
1506 found->second.hasSplinePath =
true;
1512 const auto found = nodes_.find(std::string(
id));
1515 if (
found->second.operation !=
"mesh.splineExtrude")
1522 for (std::size_t i = 0; i < profile.
points.size(); ++i) {
1524 const auto& next = profile.
points[(i + 1u) % profile.
points.size()];
1525 if (!std::isfinite(
point.x) || !std::isfinite(
point.y))
1528 if ((profile.
closed || i + 1u < profile.
points.size()) &&
1529 std::hypot(next.x -
point.x, next.y -
point.y) < 1e-6f)
1534 auto triangles = triangulateProfile(profile);
1537 found->second.splineProfile = profile;
1538 found->second.hasSplineProfile =
true;
1544 if (!std::isfinite(
value))
1547 const auto found = nodes_.find(std::string(
id));
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")
1561 const auto found = nodes_.find(std::string(
id));
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")
1575 const auto found = nodes_.find(std::string(
id));
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")
1588Result<void> MeshModifierGraph::validateNode(std::string_view
id, std::unordered_map<std::string, int>& states)
const {
1589 const std::string
key(
id);
1593 const auto found = nodes_.find(
key);
1598 if (
node.operation ==
"mesh.input") {
1599 if (!
node.hasInputMesh)
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")
1619 if ((
node.operation ==
"deform.splinePath" ||
node.operation ==
"mesh.splineTube" ||
1620 node.operation ==
"mesh.splineRibbon" ||
node.operation ==
"mesh.splineExtrude") &&
1621 !
node.hasSplinePath)
1624 if (
node.hasSplinePath &&
node.splinePath.chunkCount() > 1) {
1625 if (
node.operation ==
"deform.splinePath")
1628 for (
int chunk = 0; chunk <
node.splinePath.chunkCount(); ++chunk) {
1629 auto path =
node.splinePath.chunkPathResult(chunk);
1633 if (
node.operation ==
"deform.splinePath" && std::abs(parameter(
node,
"scale", 1.f)) < 1e-7f)
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)
1642 if (pathSegments < 1 || pathSegments > 4096)
1645 if (radialSegments < 3 || radialSegments > 256)
1648 const std::uint64_t capVertices =
node.splinePath.isClosed() || intParameter(
node,
"cap", 1) == 0
1650 : 2u *
static_cast<std::uint64_t
>(radialSegments + 2);
1652 static_cast<std::uint64_t
>(pathSegments + 1) *
static_cast<std::uint64_t
>(radialSegments + 1) + capVertices;
1657 if (
node.operation ==
"mesh.splineRibbon") {
1658 const int pathSegments = intParameter(
node,
"pathSegments", 32);
1659 if (parameter(
node,
"width", 2.f) <= 0.f)
1662 if (parameter(
node,
"thickness", 0.f) < 0.f)
1665 if (pathSegments < 1 || pathSegments > 4096)
1669 if (
node.operation ==
"mesh.splineExtrude") {
1670 if (!
node.hasSplineProfile)
1673 const int pathSegments = intParameter(
node,
"pathSegments", 32);
1674 if (pathSegments < 1 || pathSegments > 4096)
1677 const std::uint64_t stride =
node.splineProfile.points.size() + (
node.splineProfile.closed ? 1u : 0
u);
1678 if (
static_cast<std::uint64_t
>(pathSegments + 1) * stride > 1'000'000u)
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))
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)
1693 if (threshold < 0.f || threshold > 1.f)
1696 if (axis !=
"x" && axis !=
"y" && axis !=
"z")
1699 if (parameter(
node,
"frequency", 1.f) < 0.f)
1703 if (
node.operation ==
"deform.effector") {
1704 const int pointCount = intParameter(
node,
"pointCount", 1);
1705 if (pointCount < 1 || pointCount > 4)
1708 if (parameter(
node,
"density", 1.f) <= 0.f)
1712 const std::string
radius =
"p" + std::to_string(
point) +
"radius";
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)
1724 if (parameter(
node,
"maxDistance", 10.f) <= 0.f)
1733 std::unordered_map<std::string, int> states;
1734 for (
const auto&
id : nodeOrder_) {
1735 auto valid = validateNode(
id, states);
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> {
1748 const auto found = nodes_.find(
id);
1749 if (
found == nodes_.end())
1757 auto result = visit(
input);
1758 if (!result.ok())
return result;
1761 order.push_back(
id);
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)
1770 std::unordered_set<std::string> assigned;
1771 std::vector<Segment> segments;
1772 for (
const auto&
id :
order) {
1773 if (assigned.contains(
id))
continue;
1775 segment.nodes.push_back(
id);
1776 assigned.insert(
id);
1777 const auto* firstSpec = findSpec(nodes_.at(
id).operation);
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);
1787 segment.fusedVertexTraversal = segment.nodes.size() > 1u;
1788 segments.push_back(std::move(segment));
1790 return Result<std::vector<Segment>>::success(std::move(segments));
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"));
1804 for (
int vertex = 0; vertex <
output.getVertexCount(); ++vertex) {
1805 const std::size_t
p = std::size_t(vertex) * 3u;
1819Result<MeshBuild> MeshModifierGraph::executeNode(
const Node&
node,
1820 const std::unordered_map<std::string, MeshBuild>& outputs)
const {
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) {
1834 for (
int vertex = 0; vertex <
output.getVertexCount(); ++vertex) {
1835 const std::size_t
p = std::size_t(vertex) * 3u;
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));
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"));
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;
1887 if (!adhered.ok())
return adhered;
1888 MeshBuild
output = std::move(adhered).takeValue();
1889 output.setMeta(
"deformer",
"deform.meshAdhere");
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"));
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"));
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"));
1916 stringParameter(
node,
"operation",
"difference"));
1918 if (
node.operation ==
"mesh.projectUv")
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"));
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;
1941 std::unordered_map<std::string, int> validationStates;
1942 auto valid = validateNode(outputId, validationStates);
1944 auto planned = compilePlan(outputId);
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();
1953 ? executeFused(segment, outputs)
1954 : executeNode(nodes_.at(segment.nodes.front()), outputs);
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));
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()) {
1974 node.cacheValid =
true;
1977 metrics_ = std::move(candidateMetrics);
1978 compiledSegmentCount_ =
static_cast<int>(plan.size());
1979 fusedOperationCount_ = candidateFused;
1980 ++executionPlanBuildCount_;
1985 for (
auto& [_,
node] : nodes_) {
1987 node.cacheValid =
false;
1990 compiledSegmentCount_ = 0;
1991 fusedOperationCount_ = 0;
1994void MeshModifierGraph::invalidate() {
2002 return index >= 0 && index < static_cast<int>(nodeOrder_.size()) ? nodeOrder_[
static_cast<std::size_t
>(
index)]
2007 const auto found = nodes_.find(std::string(
id));
2008 return found == nodes_.end() ? std::string() :
found->second.operation;
2013 return index >= 0 && index < operationCount() ? operationSpecs()[static_cast<std::size_t>(
index)].id
2018 return spec ? spec->inputs : -1;
2022 return spec ?
static_cast<int>(spec->params.size()) : 0;
2026 return spec &&
index >= 0 && index < static_cast<int>(spec->params.size())
2027 ? spec->params[
static_cast<std::size_t
>(
index)].
key
2032 return spec &&
index >= 0 && index < static_cast<int>(spec->params.size())
2033 ? spec->params[
static_cast<std::size_t
>(
index)].
kind
2038 return spec &&
index >= 0 && index < static_cast<int>(spec->params.size())
ActionParameterOperation operation
std::map< std::string, Var > values
std::uint32_t vertexCount
std::array< double, 10 > q
std::vector< std::uint32_t > indices
std::vector< float > normals
std::vector< float > positions
std::array< float, 3 > position
std::array< float, 3 > scale
graphics::Canvas * previous
std::vector< Point > vertices
std::shared_ptr< const std::vector< glm::vec2 > > points
std::unordered_map< std::uint32_t, std::uint32_t > remap
std::vector< float > colors
RoadLaneDirection direction
std::map< Cell, int > best
const UnitySourceAsset & source
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.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
bool ok() const noexcept
Whether this result represents a non-failure outcome.
const Status & status() const noexcept
Inspect the structured operation status.
static Result failure(Status status)
Construct a failed result from a structured status.
CPU triangle mesh from procedural mesh recipes (e.g. marching cubes). Positions/normals are xyz-packe...
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.
const char * defaultValue
std::vector< ParamSpec > params
Vec3 interpolate(Vec3 start, Vec3 end, float amount) noexcept
Interpolate.
eve::Diagnostic Diagnostic
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
double sample(const Heightmap &map, double u, double v)
Sample.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
double cross(const Vec2 &a, const Vec2 &b)
Cross.
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 ¶ms)
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.
@ TypeMismatch
A stable reference resolved to a different canonical domain type.
Owning cross-section snapshot used by mesh.splineExtrude.
std::vector< SplineProfilePoint > points