14constexpr float kEpsilon = 1e-8f;
17 float x = 0.f,
y = 0.f,
z = 0.f;
23float dot(Vec3
a, Vec3
b) {
return a.x *
b.x +
a.y *
b.y +
a.z *
b.z; }
25 return {
a.y *
b.z -
a.z *
b.y,
a.z *
b.x -
a.x *
b.z,
a.x *
b.y -
a.y *
b.x};
28Vec3 normalized(Vec3
v) {
29 const float len = std::sqrt(
length2(
v));
30 return len > kEpsilon ?
v * (1.f / len) :
Vec3{0.f, 1.f, 0.f};
32float saturate(
float value) {
return std::clamp(
value, 0.f, 1.f); }
34float evaluateFalloff(std::string_view
kind,
float t) {
36 if (
kind ==
"linear")
return t;
37 if (
kind ==
"sharp")
return t *
t;
38 if (
kind ==
"sphere")
return std::sqrt(std::max(0.f, 2.f *
t -
t *
t));
39 return t *
t * (3.f - 2.f *
t);
43 float distance = std::numeric_limits<float>::infinity();
48ClosestHit closestOnTriangle(Vec3
point, Vec3
a, Vec3
b, Vec3
c) {
50 const float d1 =
dot(
ab, ap), d2 =
dot(
ac, ap);
54 const float d3 =
dot(
ab, bp), d4 =
dot(
ac, bp);
56 const float vc = d1 * d4 - d3 * d2;
57 if (vc <= 0.f && d1 >= 0.f && d3 <= 0.f) {
58 const float v = d1 / (d1 - d3);
63 const float d5 =
dot(
ab, cp), d6 =
dot(
ac, cp);
65 const float vb = d5 * d2 - d1 * d6;
66 if (vb <= 0.f && d2 >= 0.f && d6 <= 0.f) {
67 const float w = d2 / (d2 - d6);
71 const float va = d3 * d6 - d5 * d4;
72 if (va <= 0.f && (d4 - d3) >= 0.f && (d5 - d6) >= 0.f) {
73 const float w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
77 const float denom = 1.f / (va + vb + vc);
78 const float v = vb * denom;
79 const float w = vc * denom;
85 Vec3
min{std::numeric_limits<float>::max(), std::numeric_limits<float>::max(),
86 std::numeric_limits<float>::max()};
87 Vec3
max{-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max(),
88 -std::numeric_limits<float>::max()};
97 void expand(
const Aabb& other) {
101 [[nodiscard]]
float distance2(Vec3
p)
const {
124 const MeshBuild*
mesh =
nullptr;
129 void build(
const MeshBuild&
source,
const std::vector<std::int32_t>& vertexSourceIds, std::int32_t
sourceId) {
134 for (
int t = 0;
t + 2 <
source.getIndexCount();
t += 3) {
135 const auto i0 =
static_cast<std::uint32_t
>(
source.getIndex(
t));
136 const auto i1 =
static_cast<std::uint32_t
>(
source.getIndex(
t + 1));
137 const auto i2 =
static_cast<std::uint32_t
>(
source.getIndex(
t + 2));
138 if (
static_cast<std::size_t
>(
i0) >= vertexSourceIds.size())
continue;
139 if (vertexSourceIds[
i0] !=
sourceId)
continue;
144 const Vec3
a{
source.getPositionX(
static_cast<int>(
i0)),
source.getPositionY(
static_cast<int>(
i0)),
145 source.getPositionZ(
static_cast<int>(
i0))};
146 const Vec3
b{
source.getPositionX(
static_cast<int>(
i1)),
source.getPositionY(
static_cast<int>(
i1)),
147 source.getPositionZ(
static_cast<int>(
i1))};
148 const Vec3
c{
source.getPositionX(
static_cast<int>(
i2)),
source.getPositionY(
static_cast<int>(
i2)),
149 source.getPositionZ(
static_cast<int>(
i2))};
150 tri.centroid = (
a +
b +
c) * (1.f / 3.f);
151 tri.bounds.expand(
a);
152 tri.bounds.expand(
b);
153 tri.bounds.expand(
c);
158 for (std::size_t i = 0; i <
order.size(); ++i)
order[i] =
static_cast<std::int32_t
>(i);
160 buildNode(0,
static_cast<std::int32_t
>(
order.size()), 0);
163 std::int32_t buildNode(std::int32_t
begin, std::int32_t
end,
int depth) {
167 for (std::int32_t i =
begin; i <
end; ++i)
168 node.bounds.expand(triangles[
static_cast<std::size_t
>(order[
static_cast<std::size_t
>(i)])].bounds);
169 const auto index =
static_cast<std::int32_t
>(
nodes.size());
175 [&](std::int32_t
a, std::int32_t
b) {
176 const auto& ca = triangles[static_cast<std::size_t>(a)].centroid;
177 const auto& cb = triangles[static_cast<std::size_t>(b)].centroid;
178 if (axis == 0) return ca.x < cb.x || (ca.x == cb.x && a < b);
179 if (axis == 1) return ca.y < cb.y || (ca.y == cb.y && a < b);
180 return ca.z < cb.z || (ca.z == cb.z && a < b);
188 void queryNode(std::int32_t
nodeIndex, Vec3
point,
float maxDistance, ClosestHit&
best)
const {
191 if (
node.bounds.distance2(
point) > maxDistance * maxDistance)
return;
192 if (
node.count > 0) {
193 for (std::int32_t i = 0; i <
node.count; ++i) {
196 const Vec3 a{
mesh->getPositionX(
static_cast<int>(tri.i0)),
mesh->getPositionY(
static_cast<int>(tri.i0)),
197 mesh->getPositionZ(
static_cast<int>(tri.i0))};
198 const Vec3 b{
mesh->getPositionX(
static_cast<int>(tri.i1)),
mesh->getPositionY(
static_cast<int>(tri.i1)),
199 mesh->getPositionZ(
static_cast<int>(tri.i1))};
200 const Vec3 c{
mesh->getPositionX(
static_cast<int>(tri.i2)),
mesh->getPositionY(
static_cast<int>(tri.i2)),
201 mesh->getPositionZ(
static_cast<int>(tri.i2))};
211 [[nodiscard]] ClosestHit closest(Vec3
point,
float maxDistance)
const {
218Result<void> validateParams(
const MeshContactBlendParams&
params) {
219 if (!std::isfinite(
params.edgeRadius) ||
params.edgeRadius < 0.f || !std::isfinite(
params.materialRadius) ||
220 params.materialRadius < 0.f || !std::isfinite(
params.strength) ||
params.strength < 0.f ||
221 !std::isfinite(
params.normalsBlend) ||
params.normalsBlend < 0.f || !std::isfinite(
params.materialBlend) ||
222 params.materialBlend < 0.f || !std::isfinite(
params.maxQueryDistance) ||
params.maxQueryDistance < 0.f ||
223 !std::isfinite(
params.surfaceOffset))
225 "contact blend parameters must be finite and non-negative",
226 "params", {},
"procgen.mesh.blend"));
227 if (
params.falloff !=
"smooth" &&
params.falloff !=
"linear" &&
params.falloff !=
"sharp" &&
228 params.falloff !=
"sphere")
230 "contact blend falloff must be smooth|linear|sharp|sphere",
231 "falloff", {},
"procgen.mesh.blend"));
235void applyHitToVertex(MeshBuild&
output, std::vector<float>&
colors,
int vertex,
const ClosestHit&
best,
236 float maxQuery,
const MeshContactBlendParams&
params) {
239 const std::size_t base =
static_cast<std::size_t
>(vertex) * 3u;
241 if (!std::isfinite(
best.distance) ||
best.distance > maxQuery) {
242 colors[
static_cast<std::size_t
>(vertex) * 4u + 3u] = 0.f;
245 float edgeWeight = 0.f;
247 const float t = 1.f - saturate(
best.distance /
params.edgeRadius);
248 edgeWeight = saturate(evaluateFalloff(
params.falloff,
t) *
params.strength);
250 float materialWeight = 0.f;
251 if (
params.materialRadius > 0.f &&
params.materialBlend > 0.f) {
252 const float t = 1.f - saturate(
best.distance /
params.materialRadius);
253 materialWeight = saturate(evaluateFalloff(
params.falloff,
t) *
params.materialBlend);
255 if (
params.softSnapPositions && edgeWeight > 0.f) {
263 }
else if (
params.normalsBlend > 0.f && edgeWeight > 0.f) {
265 const float w = saturate(edgeWeight *
params.normalsBlend);
266 const Vec3 n1 = normalized(n0 * (1.f -
w) +
best.normal *
w);
271 colors[
static_cast<std::size_t
>(vertex) * 4u + 3u] = materialWeight;
274void recalculateNormalsFromGeometry(MeshBuild&
mesh) {
279 for (std::size_t i = 0; i + 2 <
indices.size(); i += 3) {
280 const std::size_t
a =
static_cast<std::size_t
>(
indices[i]) * 3u;
281 const std::size_t
b =
static_cast<std::size_t
>(
indices[i + 1u]) * 3u;
282 const std::size_t
c =
static_cast<std::size_t
>(
indices[i + 2u]) * 3u;
288 for (
const auto vertex : {
a,
b,
c}) {
290 normals[vertex + 1u] += face.y;
291 normals[vertex + 2u] += face.z;
294 for (std::size_t i = 0; i + 2 <
normals.size(); i += 3) {
307 std::vector<std::vector<std::uint32_t>> neighbors(
static_cast<std::size_t
>(
count));
309 for (std::size_t i = 0; i + 2 <
indices.size(); i += 3) {
311 neighbors[
a].push_back(
b);
312 neighbors[
a].push_back(
c);
313 neighbors[
b].push_back(
a);
314 neighbors[
b].push_back(
c);
315 neighbors[
c].push_back(
a);
316 neighbors[
c].push_back(
b);
320 for (
int iteration = 0; iteration <
iterations; ++iteration) {
322 for (
int vertex = 0; vertex <
count; ++vertex) {
323 const auto& adjacent = neighbors[
static_cast<std::size_t
>(vertex)];
324 Vec3 sum{
current[
static_cast<std::size_t
>(vertex) * 3u],
325 current[
static_cast<std::size_t
>(vertex) * 3u + 1u],
326 current[
static_cast<std::size_t
>(vertex) * 3u + 2u]};
327 for (
const auto neighbor : adjacent) {
328 sum.x +=
current[
static_cast<std::size_t
>(neighbor) * 3u];
329 sum.y +=
current[
static_cast<std::size_t
>(neighbor) * 3u + 1u];
330 sum.z +=
current[
static_cast<std::size_t
>(neighbor) * 3u + 2u];
332 const Vec3 n = normalized(sum);
333 const std::size_t base =
static_cast<std::size_t
>(vertex) * 3u;
335 next[base + 1u] =
n.y;
336 next[base + 2u] =
n.z;
342void finalizeSoftSnapNormals(MeshBuild&
mesh) {
343 recalculateNormalsFromGeometry(
mesh);
344 smoothNormalsNeighborhood(
mesh, 12);
345 mesh.setMeta(
"contactBlend.normals",
"recalculated+smoothed");
351 if (
mesh.getVertexCount() <= 0)
return;
352 finalizeSoftSnapNormals(
mesh);
357Result<MeshBuild> blendMultiImpl(
const MeshBuild&
mesh,
const std::vector<std::int32_t>& vertexSourceIds,
358 const MeshContactBlendParams&
params) {
363 "vertexSourceIds", {},
"procgen.mesh.blend"));
364 auto validated = validateParams(
params);
367 float maxQuery =
params.maxQueryDistance;
368 if (maxQuery <= 0.f) maxQuery = std::max(
params.edgeRadius,
params.materialRadius);
369 if (maxQuery <= 0.f) {
370 MeshBuild copy =
mesh;
371 copy.setMeta(
"contactBlend",
"identity");
375 std::vector<std::int32_t> uniqueSources = vertexSourceIds;
376 std::sort(uniqueSources.begin(), uniqueSources.end());
377 uniqueSources.erase(std::unique(uniqueSources.begin(), uniqueSources.end()), uniqueSources.end());
378 if (uniqueSources.size() < 2)
381 "vertexSourceIds", {},
"procgen.mesh.blend"));
383 std::vector<SurfaceBvh> bvhs(uniqueSources.size());
384 for (std::size_t i = 0; i < uniqueSources.size(); ++i)
385 bvhs[i].build(
mesh, vertexSourceIds, uniqueSources[i]);
388 std::vector<float>
colors =
389 mesh.hasVertexColors() ?
mesh.colors()
390 : std::vector<float>(
static_cast<std::size_t
>(
vertexCount) * 4u, 1.f);
392 for (
int vertex = 0; vertex <
vertexCount; ++vertex) {
393 const auto sourceId = vertexSourceIds[
static_cast<std::size_t
>(vertex)];
394 const std::size_t base =
static_cast<std::size_t
>(vertex) * 3u;
397 for (std::size_t i = 0; i < uniqueSources.size(); ++i) {
398 if (uniqueSources[i] ==
sourceId)
continue;
399 auto hit = bvhs[i].closest(
origin, maxQuery);
405 auto setColors =
output.setVertexColors(std::move(
colors));
407 if (
params.softSnapPositions) finalizeSoftSnapNormals(
output);
408 output.setMeta(
"contactBlend",
"applied");
409 output.setMeta(
"contactBlend.falloff", std::string(
params.falloff));
413Result<MeshBuild> blendAgainstImpl(
const MeshBuild& movable,
const MeshBuild& surface,
414 const MeshContactBlendParams&
params) {
417 "contact blend movable mesh is empty",
"movable", {},
418 "procgen.mesh.blend"));
421 "contact blend surface mesh is empty",
"surface", {},
422 "procgen.mesh.blend"));
423 auto validated = validateParams(
params);
426 float maxQuery =
params.maxQueryDistance;
427 if (maxQuery <= 0.f) maxQuery = std::max(
params.edgeRadius,
params.materialRadius);
428 if (maxQuery <= 0.f) {
429 MeshBuild copy = movable;
430 copy.setMeta(
"contactBlend",
"identity");
435 std::vector<std::int32_t> surfaceIds(
static_cast<std::size_t
>(surface.getVertexCount()), 1);
437 bvh.build(surface, surfaceIds, 1);
438 if (bvh.triangles.empty())
440 "contact blend surface has no triangles",
"surface", {},
441 "procgen.mesh.blend"));
443 MeshBuild
output = movable;
445 std::vector<float>
colors =
446 movable.hasVertexColors() ? movable.colors()
447 : std::vector<float>(
static_cast<std::size_t
>(
vertexCount) * 4u, 1.f);
449 for (
int vertex = 0; vertex <
vertexCount; ++vertex) {
450 const std::size_t base =
static_cast<std::size_t
>(vertex) * 3u;
454 auto setColors =
output.setVertexColors(std::move(
colors));
456 if (
params.softSnapPositions) finalizeSoftSnapNormals(
output);
457 output.setMeta(
"contactBlend",
"againstSurface");
458 output.setMeta(
"contactBlend.falloff", std::string(
params.falloff));
466 return blendMultiImpl(
mesh, vertexSourceIds,
params);
471 return blendAgainstImpl(movable, surface,
params);
std::uint32_t vertexCount
std::vector< std::uint32_t > indices
std::vector< float > normals
std::vector< float > positions
std::vector< float > colors
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.
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::vector< ParamSpec > params
float length2(float x, float z)
Length 2.
Vec3 operator-(Vec3 lhs, Vec3 rhs)
Operator -.
Vec3 operator+(Vec3 lhs, Vec3 rhs)
Operator +.
Vec3 operator*(Vec3 value, float scale)
Operator *.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
double dot(const Vec2 &a, const Vec2 &b)
Dot.
double cross(const Vec2 &a, const Vec2 &b)
Cross.
Result< MeshBuild > meshContactBlendResult(const MeshBuild &mesh, const std::vector< std::int32_t > &vertexSourceIds, const MeshContactBlendParams ¶ms)
Fuse edge normals / optional positions and material blend weights across source slices.
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).
int axis(int64_t a, size_t rank)
Axis.
void faceNormal(FaceDir d, float &nx, float &ny, float &nz)
Outward unit normal for the face.