#pragma once /** * @file sdf_operations.h * @brief SDF 布尔运算、修饰算子与域变形 * * 本文件提供三类操作: * * 1. **布尔运算(CSG)** — 在距离值上执行组合逻辑 * 2. **修饰算子(Modifiers)** — 对距离值做数值变换 * 3. **域变形(Domain Deformation)** — 对采样点空间做几何变换 * * 所有操作遵循 SDF 惯例:对采样点或其距离值做变换后,保持 f<0=内部、f>0=外部的语义。 * 域变形算子应用到采样点(而非对象),因此位移/旋转方向与直觉相反(逆变换)。 * * @ingroup sdf */ #include "vde/core/point.h" #include #include namespace vde::sdf { using core::Point3D; using core::Vector3D; // ── Boolean operations on distance values ── /** * @brief 布尔并集: A ∪ B * * 取两个距离值的最小值。在两个子对象的距离场中选择较近者。 * * **数学公式:** f = min(d1, d2) * * 最常见的 CSG 组合操作,用于合并两个几何体。 * * @param d1 第一个对象的 SDF 值 * @param d2 第二个对象的 SDF 值 * @return 并集的 SDF 值 * * @code{.cpp} * double d = op_union(sphere(p, 1.0), box(p, Point3D(0.5, 0.5, 0.5))); * @endcode * * @see op_smooth_union 带平滑过渡的并集 * @see op_intersection 交集 */ [[nodiscard]] inline double op_union(double d1, double d2) { return std::min(d1, d2); } /** * @brief 布尔交集: A ∩ B * * 取两个距离值的最大值。保留同时属于两个对象的区域。 * * **数学公式:** f = max(d1, d2) * * @param d1 第一个对象的 SDF 值 * @param d2 第二个对象的 SDF 值 * @return 交集的 SDF 值 * * @code{.cpp} * // 球体与盒子的交集(取共同区域) * double d = op_intersection(sphere(p, 1.0), box(p, Point3D(0.5, 0.5, 0.5))); * @endcode * * @see op_smooth_intersection 带平滑过渡的交集 */ [[nodiscard]] inline double op_intersection(double d1, double d2) { return std::max(d1, d2); } /** * @brief 布尔差集: A \ B * * 从对象 A 中减去对象 B 所占区域。 * * **数学公式:** f = max(d1, -d2) * * 将第二个距离值取反后取最大值:B 的外部变成内部,内部变成外部。 * * @param d1 被减对象 A 的 SDF 值 * @param d2 减去对象 B 的 SDF 值 * @return 差集的 SDF 值 * * @code{.cpp} * // 从球体中减去盒子 * double d = op_difference(sphere(p, 1.0), box(p, Point3D(0.3, 0.3, 0.3))); * @endcode * * @see op_smooth_difference 带平滑过渡的差集 */ [[nodiscard]] inline double op_difference(double d1, double d2) { return std::max(d1, -d2); } /** * @brief 平滑并集(带混合过渡) * * 通过多项式混合函数在两个对象接合处产生平滑圆角过渡。 * * **数学公式:** * h = clamp(0.5 + 0.5·(d1-d2)/k, 0, 1) * f = d1·(1-h) + d2·h - k·h·(1-h) * * 混合参数 k 控制过渡区域的宽度。k 越大,过渡越平滑但越偏离原始几何。 * * @param d1 第一个对象的 SDF 值 * @param d2 第二个对象的 SDF 值 * @param k 混合强度参数(> 0 时平滑过渡;≤ 0 时退化为普通并集) * @return 平滑混合后的 SDF 值 * * @note 多项式混合是函数 f(x)=x 在 [0,k] 区间的平滑近似。在 d1=d2 处,f 向下偏移 k/4。 * @warning k 值过大(超过对象尺寸的 10%)会严重改变几何形态。 * * @code{.cpp} * // 两个球体平滑融合,过渡区宽度 0.3 * double d = op_smooth_union(sphere(p, 1.0), sphere(p - Point3D(1.2,0,0), 1.0), 0.3); * @endcode * * @see op_union 无平滑的标准并集 * @see op_smooth_intersection 平滑交集 * @see op_smooth_difference 平滑差集 */ [[nodiscard]] inline double op_smooth_union(double d1, double d2, double k) { if (k <= 0.0) return std::min(d1, d2); double h = std::clamp(0.5 + 0.5 * (d1 - d2) / k, 0.0, 1.0); return d1 * (1.0 - h) + d2 * h - k * h * (1.0 - h); } /** * @brief 平滑交集(带混合过渡) * * 与平滑并集类似,但应用于交集操作。 * * **数学公式:** * h = clamp(0.5 - 0.5·(d1-d2)/k, 0, 1) * f = d1·(1-h) + d2·h + k·h·(1-h) * * @param d1 第一个对象的 SDF 值 * @param d2 第二个对象的 SDF 值 * @param k 混合强度参数 * @return 平滑混合后的 SDF 值 * * @see op_intersection 无平滑的标准交集 * @see op_smooth_union 平滑并集 */ [[nodiscard]] inline double op_smooth_intersection(double d1, double d2, double k) { if (k <= 0.0) return std::max(d1, d2); double h = std::clamp(0.5 - 0.5 * (d1 - d2) / k, 0.0, 1.0); return d1 * (1.0 - h) + d2 * h + k * h * (1.0 - h); } /** * @brief 平滑差集(带混合过渡) * * 与平滑并集类似,但应用于差集操作。 * * **数学公式:** * h = clamp(0.5 - 0.5·(d2+d1)/k, 0, 1) * f = d1·(1-h) + (-d2)·h + k·h·(1-h) * * @param d1 被减对象 A 的 SDF 值 * @param d2 减去对象 B 的 SDF 值 * @param k 混合强度参数 * @return 平滑混合后的 SDF 值 * * @see op_difference 无平滑的标准差集 * @see op_smooth_union 平滑并集 */ [[nodiscard]] inline double op_smooth_difference(double d1, double d2, double k) { if (k <= 0.0) return std::max(d1, -d2); double h = std::clamp(0.5 - 0.5 * (d2 + d1) / k, 0.0, 1.0); return d1 * (1.0 - h) + (-d2) * h + k * h * (1.0 - h); } // ── Modifiers (operate on a distance value) ── /** * @brief 圆角偏移: 将等值面向外扩展 r 单位 * * **数学公式:** f = d - r * * 相当于对隐式曲面做正向膨胀。配合其他运算可实现倒圆角效果。 * 例如 box SDF + op_round 即等价于 round_box。 * * @param d SDF 值 * @param r 偏移距离(正值 = 膨胀,负值 = 收缩) * @return 偏移后的 SDF 值 * * @code{.cpp} * // 给任意形状加 0.1 的圆角 * double rounded = op_round(my_sdf(p), 0.1); * @endcode * * @see op_onion 壳层修饰 * @see round_box 特例化的圆角立方体 */ [[nodiscard]] inline double op_round(double d, double r) { return d - r; } /** * @brief 洋葱皮/壳层修饰: 取绝对值后减去厚度 * * **数学公式:** f = |d| - thickness * * 将 SDF 的等值面变为一个厚度为 2·thickness 的壳层。 * 原始表面变成壳层的内外边界。 * * @param d SDF 值 * @param thickness 壳层半厚度,必须 > 0 * @return 壳层的 SDF 值 * * @code{.cpp} * // 0.05 厚的球壳 * double shell = op_onion(sphere(p, 1.0), 0.05); * @endcode * * @see op_round 圆角偏移 */ [[nodiscard]] inline double op_onion(double d, double thickness) { return std::abs(d) - thickness; } // ── Domain deformation operators (transform point space) ── /** * @brief 无限重复(周期复制) * * 将空间映射到以 cell 为周期的单位晶格中。 * 每个晶格是边长为 cell 的轴对齐长方体。 * * **变换:** p' = p - cell·round(p / cell) * * 配合任何 SDF 使用,可生成无限周期阵列。 * * @param p 原始采样点 * @param cell 晶格尺寸 (cx, cy, cz),各分量必须 > 0 才生效 * @return 映射到中心晶格内的点 * * @code{.cpp} * // 在空间中无限重复的球体阵列(间距 2) * Point3D q = op_repeat(p, Point3D(2, 2, 2)); * double d = sphere(q, 0.5); * @endcode * * @note 边界处 SDF 可能不连续,需要确保各晶格间映射是连续的。 */ [[nodiscard]] inline Point3D op_repeat(const Point3D& p, const Point3D& cell) { auto wrap = [](double v, double c) { return c > 0.0 ? v - c * std::round(v / c) : v; }; return Point3D(wrap(p.x(), cell.x()), wrap(p.y(), cell.y()), wrap(p.z(), cell.z())); } /** * @brief 镜像对称(X=0 平面),可选偏移 * * 将空间关于 x = offset 平面取镜像对称。 * * **变换:** p'_x = offset + |p_x - offset| * * @param p 原始采样点 * @param offset 镜像平面沿 X 轴的偏移量(默认 0 = X=0 平面) * @return 映射到镜像半空间的点 * * @code{.cpp} * // 关于 X=1 平面对称 * Point3D q = op_mirror_x(p, 1.0); * @endcode * * @see op_mirror_y YZ 平面镜像 * @see op_mirror_z XY 平面镜像 */ [[nodiscard]] inline Point3D op_mirror_x(const Point3D& p, double offset = 0.0) { return Point3D(offset + std::abs(p.x() - offset), p.y(), p.z()); } /** * @brief 镜像对称(Y=0 平面) * * 将空间关于 XZ 平面取镜像对称。 * * **变换:** p'_y = |p_y| * * @param p 原始采样点 * @return 映射到上半空间的点 * * @code{.cpp} * // 关于 Y=0 对称(上下对称) * Point3D q = op_mirror_y(p); * double d = sphere(q - Point3D(0, 1.0, 0), 0.5); // 上下各一个球 * @endcode */ [[nodiscard]] inline Point3D op_mirror_y(const Point3D& p) { return Point3D(p.x(), std::abs(p.y()), p.z()); } /** * @brief 镜像对称(Z=0 平面) * * 将空间关于 XY 平面取镜像对称。 * * **变换:** p'_z = |p_z| * * @param p 原始采样点 * @return 映射到前半空间的点 */ [[nodiscard]] inline Point3D op_mirror_z(const Point3D& p) { return Point3D(p.x(), p.y(), std::abs(p.z())); } /** * @brief 空间平移(逆变换) * * 将采样点向反方向平移,使后续 SDF 求值表现出正向移动效果。 * * **变换:** p' = p - offset * * 例如 offset = (2, 0, 0) 会使 SDF 在空间中表现为向右移动 2 个单位。 * * @param p 原始采样点 * @param offset 平移量(对象正向移动的方向) * @return 逆平移后的采样点 * * @code{.cpp} * // 球体向右平移 2 个单位 * Point3D q = op_translate(p, Point3D(2, 0, 0)); * double d = sphere(q, 1.0); * @endcode * * @see op_rotate 旋转变换 * @see op_scale 缩放变换 */ [[nodiscard]] inline Point3D op_translate(const Point3D& p, const Point3D& offset) { return p - offset; } /** * @brief 绕 Y 轴旋转(逆变换) * * 将采样点绕 Y 轴旋转 -angle_rad,使后续 SDF 表现出正向旋转效果。 * * **变换矩阵:** * ``` * p'_x = p_x·cos(θ) + p_z·sin(θ) * p'_y = p_y * p'_z = -p_x·sin(θ) + p_z·cos(θ) * ``` * * @param p 原始采样点 * @param angle_rad 旋转角度(弧度),正值 = 逆时针(从上往下看) * @return 逆旋转后的采样点 * * @code{.cpp} * // 将盒子绕 Y 轴旋转 45° * Point3D q = op_rotate(p, M_PI / 4); * double d = box(q, Point3D(1, 1, 1)); * @endcode * * @see op_twist 绕 Y 轴的扭曲变换 * @see op_bend 弯曲变换 */ [[nodiscard]] inline Point3D op_rotate(const Point3D& p, double angle_rad) { double c = std::cos(-angle_rad); double s = std::sin(-angle_rad); return Point3D(p.x() * c - p.z() * s, p.y(), p.x() * s + p.z() * c); } /** * @brief 非均匀缩放(逆变换) * * 将采样点各分量分别除以缩放因子。 * * **变换:** p'_i = p_i / s_i * * @param p 原始采样点 * @param s 各轴缩放因子 (sx, sy, sz),均必须 ≠ 0 * @return 缩放后的采样点 * * @warning 非均匀缩放会破坏 SDF 的距离性质(梯度不再是单位长度)。 * 只有均匀缩放(sx=sy=sz)才能保持距离场精确性。 * * @code{.cpp} * // 均匀缩小为一半 * Point3D q = op_scale(p, Point3D(2, 2, 2)); * double d = sphere(q, 1.0); // 实际半径变为 0.5 * @endcode * * @see op_translate 平移(等距变换) * @see op_rotate 旋转(等距变换) */ [[nodiscard]] inline Point3D op_scale(const Point3D& p, const Point3D& s) { return Point3D(p.x() / s.x(), p.y() / s.y(), p.z() / s.z()); } /** * @brief 绕 Y 轴的扭曲变换 * * 旋转角度随 Y 坐标线性变化: angle = amount · p_y。 * * **变换:** * ``` * p'_x = p_x·cos(amount·p_y) - p_z·sin(amount·p_y) * p'_y = p_y * p'_z = p_x·sin(amount·p_y) + p_z·cos(amount·p_y) * ``` * * @param p 原始采样点 * @param amount 单位高度上的旋转量(弧度/单位长度) * @return 扭曲后的采样点 * * @code{.cpp} * // 在上方 2 个单位处旋转 90 度的扭曲 * Point3D q = op_twist(p, M_PI / 4); * double d = box(q, Point3D(0.5, 2, 0.5)); * @endcode * * @see op_rotate 恒定角度旋转 * @see op_bend 弯曲变换 */ [[nodiscard]] inline Point3D op_twist(const Point3D& p, double amount) { double c = std::cos(amount * p.y()); double s = std::sin(amount * p.y()); return Point3D(p.x() * c - p.z() * s, p.y(), p.x() * s + p.z() * c); } /** * @brief 弯曲变换(绕 Z 轴弯曲 XZ 平面) * * 将空间沿 X 轴弯曲,弯曲量由参数 k 控制。 * * **变换:** * ``` * p'_x = cos(k·p_x)·p_x - sin(k·p_x)·p_y * p'_y = sin(k·p_x)·p_x + cos(k·p_x)·p_y * p'_z = p_z * ``` * * @param p 原始采样点 * @param k 弯曲曲率参数(k 越大弯曲越剧烈) * @return 弯曲后的采样点 * * @code{.cpp} * // 轻度弯曲的长方体 * Point3D q = op_bend(p, 0.2); * double d = box(q, Point3D(3, 0.3, 0.3)); * @endcode * * @see op_cheap_bend 简化版弯曲(仅绕 X 轴) * @see op_twist 扭曲变换 */ [[nodiscard]] inline Point3D op_bend(const Point3D& p, double k) { double c = std::cos(k * p.x()); double s = std::sin(k * p.x()); return Point3D(c * p.x() - s * p.y(), s * p.x() + c * p.y(), p.z()); } /** * @brief 拉伸/压扁域变形(消除拉伸方向上的内部体积) * * 将点沿各主轴方向向原点压缩,消除 |p_i| ≤ h_i 区域的内部信息。 * 这在组合建模中用于从对象中挖去沿坐标轴的孔道。 * * **变换:** * 对每个分量 i: * - 若 |p_i| > h_i: p'_i = p_i - sign(p_i)·h_i * - 否则: p'_i = 0 * * @param p 原始采样点 * @param h 各轴的拉伸阈值 (hx, hy, hz),均 ≥ 0 * @return 变形后的采样点 * * @code{.cpp} * // 将球体沿 X 轴拉伸,挖去|x|≤1 的区域 * Point3D q = op_elongate(p, Point3D(1, 0, 0)); * double d = sphere(q, 1.5); * @endcode * * @see op_repeat 周期重复 */ [[nodiscard]] inline Point3D op_elongate(const Point3D& p, const Point3D& h) { Point3D q = p.cwiseAbs() - h; return Point3D( q.x() > 0.0 ? p.x() - std::copysign(h.x(), p.x()) : 0.0, q.y() > 0.0 ? p.y() - std::copysign(h.y(), p.y()) : 0.0, q.z() > 0.0 ? p.z() - std::copysign(h.z(), p.z()) : 0.0 ); } /** * @brief 简化弯曲变换(绕 X 轴弯曲 YZ 平面) * * 比 op_bend 更快但精度较低的弯曲,仅在 YZ 方向产生偏移。 * * **变换:** * ``` * p'_x = p_x * p'_y = p_y·cos(k·p_x) - p_z·sin(k·p_x) * p'_z = p_y·sin(k·p_x) + p_z·cos(k·p_x) * ``` * * @param p 原始采样点 * @param k 弯曲曲率参数 * @return 弯曲后的采样点 * * @note 使用场景:不需要精确物理弯曲,仅需视觉效果时优先用此函数以节省计算。 * * @see op_bend 完整弯曲变换 */ [[nodiscard]] inline Point3D op_cheap_bend(const Point3D& p, double k) { double c = std::cos(k * p.x()); double s = std::sin(k * p.x()); return Point3D(p.x(), p.y() * c - p.z() * s, p.y() * s + p.z() * c); } /** * @brief 正弦波位移扰动 * * 在距离场上叠加一个三维正弦波的微小扰动。 * * **数学公式:** * f = d + amplitude · ∏_i sin(p_i · frequency) * * 三个方向的 sin 函数乘积产生体纹理效果。 * * @param d 原始 SDF 值 * @param p 采样点(用于扰动计算) * @param amplitude 扰动幅值 * @param frequency 空间频率 * @return 扰动后的 SDF 值 * * @warning 扰动能轻易破坏距离场的 Lipschitz 性质(|∇f| ≤ 1), * 导致 ray-marching 效率下降。需控制 amplitude/frequency 在合理范围内。 * * @code{.cpp} * // 给球体添加微小波纹 * double d = op_displace(sphere(p, 1.0), p, 0.05, 4.0); * @endcode */ [[nodiscard]] inline double op_displace(double d, const Point3D& p, double amplitude, double frequency) { double noise = std::sin(p.x() * frequency) * std::sin(p.y() * frequency) * std::sin(p.z() * frequency); return d + amplitude * noise; } } // namespace vde::sdf