Files
ViewDesignEngine/include/vde/sdf/sdf_operations.h
T
茂之钳 4c9ee4f760
CI / Build & Test (push) Failing after 29s
CI / Release Build (push) Failing after 38s
docs: doxygen annotations for curves + mesh + sketch
2026-07-24 11:04:04 +00:00

554 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 <cmath>
#include <algorithm>
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