feat(sdf): S11-B — SDF operations + domain deformations
Add header-only SDF combinator operations and domain deformation functions based on Inigo Quilez standard formulations: Boolean operations: - op_union (min), op_intersection (max), op_difference (max(-d1,d2)) - Smooth variants: op_smooth_union/intersection/difference with blend radius k with graceful fallback to sharp when k <= 0 Shape modifiers: - op_round (d - r), op_onion (abs(d) - t/2) Domain deformations (coordinate remapping): - op_repeat (infinite grid repetition) - op_mirror_x/y/z (with optional offset) - op_rotate (Y-axis), op_translate, op_scale - op_twist (Y-axis), op_bend, op_cheap_bend - op_elongate, op_displace All functions are inline in include/vde/sdf/sdf_operations.h. Comprehensive unit tests in tests/sdf/test_sdf_operations.cpp covering sharp/smooth booleans, modifiers, domain ops, and composition tests.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include "vde/core/point.h"
|
||||
|
||||
namespace vde::sdf {
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Boolean Operations (sharp)
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
/// Union (min) — combine two shapes
|
||||
[[nodiscard]] inline double op_union(double d1, double d2) {
|
||||
return std::min(d1, d2);
|
||||
}
|
||||
|
||||
/// Intersection (max) — region common to both shapes
|
||||
[[nodiscard]] inline double op_intersection(double d1, double d2) {
|
||||
return std::max(d1, d2);
|
||||
}
|
||||
|
||||
/// Difference — subtract d2 from d1 (d1 \ d2)
|
||||
[[nodiscard]] inline double op_difference(double d1, double d2) {
|
||||
return std::max(-d1, d2);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Smooth Boolean Operations (Inigo Quilez)
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
namespace detail {
|
||||
template <typename T>
|
||||
[[nodiscard]] constexpr T mix(T a, T b, T t) noexcept {
|
||||
return a + t * (b - a);
|
||||
}
|
||||
}
|
||||
|
||||
/// Smooth union with blend radius k
|
||||
/// Falls back to sharp union when k <= 0
|
||||
[[nodiscard]] inline double op_smooth_union(double d1, double d2, double k) {
|
||||
if (k <= 0.0) return op_union(d1, d2);
|
||||
double h = std::clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
|
||||
return detail::mix(d2, d1, h) - k * h * (1.0 - h);
|
||||
}
|
||||
|
||||
/// Smooth intersection with blend radius k
|
||||
/// Falls back to sharp intersection when k <= 0
|
||||
[[nodiscard]] inline double op_smooth_intersection(double d1, double d2, double k) {
|
||||
if (k <= 0.0) return op_intersection(d1, d2);
|
||||
double h = std::clamp(0.5 - 0.5 * (d2 - d1) / k, 0.0, 1.0);
|
||||
return detail::mix(d1, d2, h) + k * h * (1.0 - h);
|
||||
}
|
||||
|
||||
/// Smooth difference with blend radius k (d1 \ d2)
|
||||
/// Falls back to sharp difference when k <= 0
|
||||
[[nodiscard]] inline double op_smooth_difference(double d1, double d2, double k) {
|
||||
if (k <= 0.0) return op_difference(d1, d2);
|
||||
double h = std::clamp(0.5 - 0.5 * (d2 + d1) / k, 0.0, 1.0);
|
||||
return detail::mix(d2, -d1, h) + k * h * (1.0 - h);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Shape Modifiers
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
/// Round a shape — shrink-wraps by radius r, then expands
|
||||
/// Equivalent to: shape_distance - r
|
||||
[[nodiscard]] inline double op_round(double d, double r) {
|
||||
return d - r;
|
||||
}
|
||||
|
||||
/// Onion — create a thin shell of thickness t
|
||||
/// Returns SDF of the shell region (t/2 inward, t/2 outward from surface)
|
||||
[[nodiscard]] inline double op_onion(double d, double thickness) {
|
||||
return std::abs(d) - thickness * 0.5;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Domain Deformations (coordinate remapping)
|
||||
// Apply BEFORE evaluating the SDF primitive.
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
/// Infinite repetition along each axis
|
||||
/// c.x/y/z: cell size per axis (0 = no repeat along that axis)
|
||||
[[nodiscard]] inline vde::core::Point3D op_repeat(const vde::core::Point3D& p, const vde::core::Point3D& c) {
|
||||
// p - c * round(p / c)
|
||||
// For axes where c_i == 0, skip repetition
|
||||
auto q = p;
|
||||
if (c.x() > 0.0) q.x() = p.x() - c.x() * std::round(p.x() / c.x());
|
||||
if (c.y() > 0.0) q.y() = p.y() - c.y() * std::round(p.y() / c.y());
|
||||
if (c.z() > 0.0) q.z() = p.z() - c.z() * std::round(p.z() / c.z());
|
||||
return q;
|
||||
}
|
||||
|
||||
/// Mirror across X=0 plane (folds everything to x >= offset)
|
||||
[[nodiscard]] inline vde::core::Point3D op_mirror_x(const vde::core::Point3D& p, double offset = 0.0) {
|
||||
return {offset + std::abs(p.x() - offset), p.y(), p.z()};
|
||||
}
|
||||
|
||||
/// Mirror across Y=0 plane (folds everything to y >= offset)
|
||||
[[nodiscard]] inline vde::core::Point3D op_mirror_y(const vde::core::Point3D& p, double offset = 0.0) {
|
||||
return {p.x(), offset + std::abs(p.y() - offset), p.z()};
|
||||
}
|
||||
|
||||
/// Mirror across Z=0 plane (folds everything to z >= offset)
|
||||
[[nodiscard]] inline vde::core::Point3D op_mirror_z(const vde::core::Point3D& p, double offset = 0.0) {
|
||||
return {p.x(), p.y(), offset + std::abs(p.z() - offset)};
|
||||
}
|
||||
|
||||
/// Rotate point around Y axis by angle (radians)
|
||||
[[nodiscard]] inline vde::core::Point3D op_rotate(const vde::core::Point3D& p, double angle_rad) {
|
||||
double s = std::sin(angle_rad);
|
||||
double c = std::cos(angle_rad);
|
||||
return {c * p.x() + s * p.z(), p.y(), -s * p.x() + c * p.z()};
|
||||
}
|
||||
|
||||
/// Translate point
|
||||
[[nodiscard]] inline vde::core::Point3D op_translate(const vde::core::Point3D& p, const vde::core::Point3D& offset) {
|
||||
return {p.x() - offset.x(), p.y() - offset.y(), p.z() - offset.z()};
|
||||
}
|
||||
|
||||
/// Non-uniform scale — divides coordinates (non-exact SDF scaling)
|
||||
/// Use with care: the resulting distance field is approximate.
|
||||
[[nodiscard]] inline vde::core::Point3D op_scale(const vde::core::Point3D& p, const vde::core::Point3D& s) {
|
||||
return {p.x() / s.x(), p.y() / s.y(), p.z() / s.z()};
|
||||
}
|
||||
|
||||
/// Twist around Y axis — rotation amount increases with y coordinate
|
||||
[[nodiscard]] inline vde::core::Point3D op_twist(const vde::core::Point3D& p, double amount) {
|
||||
double angle = amount * p.y();
|
||||
double s = std::sin(angle);
|
||||
double c = std::cos(angle);
|
||||
return {c * p.x() + s * p.z(), p.y(), -s * p.x() + c * p.z()};
|
||||
}
|
||||
|
||||
/// Bend along Y axis — curves the shape in x-y plane
|
||||
[[nodiscard]] inline vde::core::Point3D op_bend(const vde::core::Point3D& p, double amount) {
|
||||
double c = std::cos(amount * p.y());
|
||||
double s = std::sin(amount * p.y());
|
||||
return {c * p.x() - s * p.y(), s * p.x() + c * p.y(), p.z()};
|
||||
}
|
||||
|
||||
/// Elongate along axes — stretches coordinates (divides by factor)
|
||||
/// Equivalent to non-uniform scale but conceptually "stretches" the shape.
|
||||
[[nodiscard]] inline vde::core::Point3D op_elongate(const vde::core::Point3D& p, const vde::core::Point3D& factors) {
|
||||
return {p.x() / factors.x(), p.y() / factors.y(), p.z() / factors.z()};
|
||||
}
|
||||
|
||||
/// Displacement — adds sinusoidal perturbation to the SDF value
|
||||
/// p: current point, d: base SDF value, amplitude/frequency: displacement params
|
||||
[[nodiscard]] inline double op_displace(double d, const vde::core::Point3D& p, double amplitude, double frequency) {
|
||||
return d + amplitude * std::sin(p.x() * frequency)
|
||||
* std::sin(p.y() * frequency)
|
||||
* std::sin(p.z() * frequency);
|
||||
}
|
||||
|
||||
/// Cheap bend — bends around Z axis based on x coordinate
|
||||
/// Simpler and faster approximation than full bend.
|
||||
[[nodiscard]] inline vde::core::Point3D op_cheap_bend(const vde::core::Point3D& p, double k) {
|
||||
double c = std::cos(k * p.x());
|
||||
double s = std::sin(k * p.x());
|
||||
return {c * p.x() - s * p.y(), s * p.x() + c * p.y(), p.z()};
|
||||
}
|
||||
|
||||
} // namespace vde::sdf
|
||||
Reference in New Issue
Block a user