Files
茂之钳 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

579 lines
20 KiB
C++
Raw Permalink 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_primitives.h
* @brief 有符号距离函数(SDF)基础图元
*
* 提供常见几何体到三维空间中各点的有符号距离计算。
* 所有图元定义在 vde::sdf 命名空间下,使用 vde::core::Point3D 作为输入。
*
* ## SDF 约定
*
* - f(p) < 0: 点 p 在几何体**内部**
* - f(p) = 0: 点 p 在几何体**表面**上
* - f(p) > 0: 点 p 在几何体**外部**
* - |f(p)| 的绝对值近似为 p 到最近表面的欧几里得距离
*
* ## 数值特性
*
* - 所有内联函数标记为 `[[nodiscard]]`,强制调用方使用返回值
* - 距离函数在表面附近满足 |∇f| ≈ 1(Lipschitz 连续,步进可靠)
* - 部分复杂图元(cone, triangular_prism, link, infinite_cone)为非内联实现,定义在 src/sdf/ 中
*
* @note 所有图元假设未施加变换——坐标系原点即图元中心/基点。
* 需要平移/旋转/缩放时,使用 sdf_operations.h 中的域变形算子对采样点预先变换。
*
* @ingroup sdf
*/
#include "vde/core/point.h"
#include <cmath>
#include <algorithm>
namespace vde::sdf {
using core::Point3D;
using core::Vector3D;
// ═══════════════════════════════════════════════
// Basic Primitives
// ═══════════════════════════════════════════════
/**
* @brief 球体的 SDF
*
* 计算三维空间中一点到球面的有符号距离。
*
* **数学公式:** f(p) = |p| - r
*
* ```
* f < 0: 点在球内
* f = 0: 点在球面上
* f > 0: 点在球外
* ```
*
* |∇f| = 1 在球外和球面上严格成立,球内在原点处退化(梯度为零)。
*
* @param p 采样点(三维坐标)
* @param radius 球半径,必须 > 0
* @return 有符号距离
*
* @note 球位于原点。如需其他位置,使用 op_translate 对采样点做反向平移。
*
* @code{.cpp}
* double d = sphere(Point3D(1.0, 0.0, 0.0), 0.5); // d = 0.5 (外部)
* double inside = sphere(Point3D(0.0, 0.0, 0.0), 1.0); // d = -1.0 (球心)
* @endcode
*
* @see ellipsoid 各轴半径不同的椭球
* @see cylinder 沿 Y 轴的圆柱
*/
[[nodiscard]] inline double sphere(const Point3D& p, double radius) {
return p.norm() - radius;
}
/**
* @brief 轴对齐立方体的 SDF
*
* 计算点到以原点为中心的轴对齐长方体表面的有符号距离。
*
* **算法概要:**
* 1. 将点的各分量取绝对值,得到第一卦限对称映射
* 2. 减去半边长得到相对于面的距离 qᵢ = |pᵢ| - halfExtentᵢ
* 3. 外部距离 = ||max(q,0)||,内部惩罚 = min(max(qₓ,q_y,q_z), 0)
*
* 结果在面外为真实欧氏距离,边/角区域使用 L2 距离组合保证连续性。
*
* @param p 采样点
* @param half_extents 半边长 (hx, hy, hz),均必须 ≥ 0
* @return 有符号距离
*
* @code{.cpp}
* // 2×3×4 的盒子
* double d = box(Point3D(0.0, 0.0, 0.0), Point3D(1.0, 1.5, 2.0));
* @endcode
*
* @see round_box 带圆角的盒子
* @see wedge 对角切割的半盒子
*/
[[nodiscard]] inline double box(const Point3D& p, const Point3D& half_extents) {
Point3D q = p.cwiseAbs() - half_extents;
return q.cwiseMax(0.0).norm()
+ std::min(std::max({q.x(), q.y(), q.z()}), 0.0);
}
/**
* @brief 圆角立方体的 SDF
*
* 与 box() 使用相同的核心算法,但在最后减去圆角半径 r。
* 相当于先对 box SDF 做膨胀(offset),再缩回,等价于对边角做半径为 r 的倒圆处理。
*
* **数学公式:** f(p) = box(p, half_extents) - r
*
* @param p 采样点
* @param half_extents 半边长 (hx, hy, hz)
* @param r 圆角半径,必须 < min(half_extents) 否则整体形态改变
* @return 有符号距离
*
* @warning 当 r 超过某轴半边长时,该维度的边会被完全圆化,表面不再平坦。
* 实际应用中限制 r ≤ 0.5 * min(hx, hy, hz) 可保证形态可控。
*
* @code{.cpp}
* // 带 0.2 圆角的 2×2×2 立方体
* double d = round_box(p, Point3D(1, 1, 1), 0.2);
* @endcode
*
* @see box 无圆角的长方体
* @see op_round 对任意 SDF 做圆角偏移
*/
[[nodiscard]] inline double round_box(const Point3D& p, const Point3D& half_extents, double r) {
Point3D q = p.cwiseAbs() - half_extents;
return q.cwiseMax(0.0).norm()
+ std::min(std::max({q.x(), q.y(), q.z()}), 0.0)
- r;
}
/**
* @brief 圆环面的 SDF
*
* 圆环位于 XZ 平面内,绕 Y 轴对称。
*
* **数学公式:**
* 1. 计算环向半径: q = √(pₓ² + p𝓏²) - R_major
* 2. 管向距离: f = √(q² + p_y²) - R_minor
*
* R_major = 环心到管心的距离(主半径)
* R_minor = 管的截面半径(副半径)
*
* @param p 采样点
* @param major_radius 主半径(环的半径),必须 > 0
* @param minor_radius 副半径(管的粗细),必须 > 0
* @return 有符号距离
*
* @warning 当 major_radius ≤ minor_radius 时,环孔消失形成苹果状(self-intersecting torus)。
*
* @code{.cpp}
* // 主半径 2.0、管半径 0.5 的圆环
* double d = torus(Point3D(2.0, 0.0, 0.0), 2.0, 0.5); // d ≈ 0.0(表面上)
* @endcode
*
* @see link 双环连接体(两个并行圆环的组合)
*/
[[nodiscard]] inline double torus(const Point3D& p, double major_radius, double minor_radius) {
double qx = std::sqrt(p.x() * p.x() + p.z() * p.z()) - major_radius;
return std::sqrt(qx * qx + p.y() * p.y()) - minor_radius;
}
/**
* @brief 胶囊体的 SDF
*
* 以线段 ab 为骨架、半径为 radius 的扫掠球体。
* 等价于线段上最近的点到 p 的距离减去 radius。
*
* **算法概要:**
* 1. 将 p 投影到线段 ab 上,投影参数 h = clamp(pa·ba / ba·ba, 0, 1)
* 2. 计算 p 到投影点的距离减去 radius
*
* 退化情况: 当 |ba| ≈ 0(a ≈ b),退化为以 a 为球心的球体。
*
* @param p 采样点
* @param a 线段起点
* @param b 线段终点
* @param radius 扫掠球半径,必须 > 0
* @return 有符号距离
*
* @code{.cpp}
* // 从 (0,-2,0) 到 (0,2,0) 的胶囊、半径 0.5
* double d = capsule(p, Point3D(0,-2,0), Point3D(0,2,0), 0.5);
* @endcode
*
* @see cylinder 两端平头的圆柱(非球形端盖)
*/
[[nodiscard]] inline double capsule(const Point3D& p, const Point3D& a, const Point3D& b, double radius) {
Point3D pa = p - a;
Point3D ba = b - a;
double ba_sq = ba.squaredNorm();
if (ba_sq < 1e-30) {
// Degenerate to sphere
return pa.norm() - radius;
}
double h = std::clamp(pa.dot(ba) / ba_sq, 0.0, 1.0);
return (pa - ba * h).norm() - radius;
}
/**
* @brief 带平端盖的圆柱体 SDF
*
* 沿 Y 轴、以原点为中心的有限长圆柱体。两端为平切面(非球形端盖)。
*
* **算法概要:**
* 1. 水平距离: d_xz = √(pₓ² + p𝓏²) - radius
* 2. 垂直距离: d_y = |p_y| - height/2
* 3. 用二维 min/max L2 组合计算矩形区域到圆角的 SDF
*
* 结果在柱面上为真实距离,在端盖边缘有连续过渡。
*
* @param p 采样点
* @param radius 圆柱截面半径,必须 > 0
* @param height 圆柱总高度(从 -h/2 到 +h/2),必须 > 0
* @return 有符号距离
*
* @code{.cpp}
* // 半径 1.0、高度 3.0 的圆柱
* double d = cylinder(Point3D(1.0, 0.0, 0.0), 1.0, 3.0);
* @endcode
*
* @see capsule 两端球形端盖的圆柱
* @see infinite_cylinder 无限长圆柱
* @see cone 圆锥台
*/
[[nodiscard]] inline double cylinder(const Point3D& p, double radius, double height) {
double d_xz = std::sqrt(p.x() * p.x() + p.z() * p.z()) - radius;
double d_y = std::abs(p.y()) - height * 0.5;
return std::min(std::max(d_xz, d_y), 0.0)
+ std::sqrt(std::max(d_xz, 0.0) * std::max(d_xz, 0.0)
+ std::max(d_y, 0.0) * std::max(d_y, 0.0));
}
/**
* @brief 平面的 SDF
*
* 通过原点的平面,法向量为 normal,偏移量为 offset。
*
* **数学公式:** f(p) = p·n - offset
*
* 其中 n 为单位法向量。f > 0 表示点在法向量指向的一侧。
* 偏移量沿法向量正方向移动平面。
*
* @param p 采样点
* @param normal 单位法向量(必须已归一化)
* @param offset 沿法向量正方向的偏移量
* @return 有符号距离
*
* @pre normal.norm() ≈ 1.0(调用方负责归一化)
*
* @code{.cpp}
* // 通过原点、法向量朝上的平面
* double d = plane(p, Vector3D(0,1,0), 0.0);
* // 向上偏移 2 个单位的平面
* double d2 = plane(p, Vector3D(0,1,0), 2.0);
* @endcode
*
* @see wedge 两个平面+盒子的组合(对角切割)
*/
[[nodiscard]] inline double plane(const Point3D& p, const Vector3D& normal, double offset) {
return p.dot(normal) - offset;
}
/**
* @brief 椭球体的 SDF(有界梯度近似)
*
* 以原点为中心的椭球,半轴长为 radii = (rx, ry, rz)。
*
* **算法概要:**
*
* 使用 bound-normalized 近似方法:
* 1. 缩放坐标: pr = p / radii
* 2. k0 = |pr|k1 = |p / radii²|
* 3. f = k0 * (k0 - 1) / k1
*
* 该方法保证在表面上 f = 0,但内部/远处的距离为近似值。
* 相比精确椭球距离,避开了六次方程的昂贵求根。
*
* @param p 采样点
* @param radii 半轴长 (rx, ry, rz),均必须 > 0
* @return 有符号距离(近似)
*
* @note 当三个半径相等时退化为球体,此时使用 sphere() 更精确高效。
* @note 当点到原点距离远大于最大半径时,近似精度下降。
*
* @code{.cpp}
* // 扁椭球: rx=2, ry=1, rz=1
* double d = ellipsoid(Point3D(2.0, 0.0, 0.0), Point3D(2, 1, 1));
* @endcode
*
* @see sphere 精确球体(rx=ry=rz 时的特例)
*/
[[nodiscard]] inline double ellipsoid(const Point3D& p, const Point3D& radii) {
Point3D pr(p.x() / radii.x(), p.y() / radii.y(), p.z() / radii.z());
double k0 = pr.norm();
double k1 = Point3D(p.x() / (radii.x() * radii.x()),
p.y() / (radii.y() * radii.y()),
p.z() / (radii.z() * radii.z())).norm();
if (k1 < 1e-30) {
// Degenerate: point at origin → distance = -min(radii)
return -std::min({radii.x(), radii.y(), radii.z()});
}
return k0 * (k0 - 1.0) / k1;
}
// ═══════════════════════════════════════════════
// Parametric Primitives
// ═══════════════════════════════════════════════
/**
* @brief 正六棱柱的 SDF
*
* 六边形位于 XZ 平面,尖顶沿 X 轴方向,沿 Y 轴挤出的正六棱柱。
*
* **算法概要:**
* 1. 将点对称映射到第一象限后,计算二维六边形的 SDF
* 2. 六边形 SDF 通过两组约束平面表示: max(px + 0.577*pz, 1.155*pz) - radius
* 3. 沿 Y 轴做 bounded extrusion
*
* @param p 采样点
* @param radius 六边形外接圆半径(中心到顶点),必须 > 0
* @param height 挤出总高度(±h/2 沿 Y 轴),必须 > 0
* @return 有符号距离
*
* @code{.cpp}
* // 外接圆半径 1.0、高度 2.0 的六棱柱
* double d = hex_prism(Point3D(0.0, 0.0, 0.0), 1.0, 2.0);
* @endcode
*
* @see extrusion_bounded 通用 bounded extrusion 算子
*/
[[nodiscard]] inline double hex_prism(const Point3D& p, double radius, double height) {
double px = std::abs(p.x());
double pz = std::abs(p.z());
// 2D hexagon SDF (pointy-top in XZ plane)
double d_hex = std::max(px + pz * 0.577350269189626, pz * 1.154700538379252) - radius;
// Extrude along Y
return std::max(d_hex, std::abs(p.y()) - height * 0.5);
}
/**
* @brief 无限长圆柱的 SDF
*
* 沿任意方向的无限长圆柱(无端盖),轴线过原点。
*
* **数学公式:** f(p) = |p × axis| - radius
*
* 叉积给出点到轴线的垂直距离。
*
* @param p 采样点
* @param axis 圆柱轴线方向(必须归一化,|axis| = 1)
* @param radius 截面半径,必须 > 0
* @return 有符号距离
*
* @pre axis.norm() ≈ 1.0
*
* @code{.cpp}
* // 沿 Z 轴的无限圆柱
* double d = infinite_cylinder(p, Vector3D(0,0,1), 0.5);
* @endcode
*
* @see cylinder 有限长圆柱(带平端盖)
*/
[[nodiscard]] inline double infinite_cylinder(const Point3D& p, const Vector3D& axis, double radius) {
return p.cross(axis).norm() - radius;
}
/**
* @brief 楔形体的 SDF
*
* 立方体沿 XZ 平面对角线切割的一半。切割面为 z = x。
* 楔形体占据盒子内 z ≥ x 的区域。
*
* **几何形状:**
* - 盒子: [-w/2, w/2] × [-h/2, h/2] × [-d/2, d/2]
* - 保留区域: 盒子内 z ≥ x 的半空间
*
* @param p 采样点
* @param w 宽度(X 方向)
* @param h 高度(Y 方向)
* @param d 深度(Z 方向)
* @return 有符号距离
*
* @code{.cpp}
* // 2×2×2 的楔形体(保留 z ≥ x 的一半)
* double d = wedge(Point3D(-0.5, 0.0, 0.5), 2.0, 2.0, 2.0);
* @endcode
*
* @see box 完整立方体
* @see plane 独立平面 SDF
*/
[[nodiscard]] inline double wedge(const Point3D& p, double w, double h, double d) {
Point3D q = p.cwiseAbs() - Point3D(w * 0.5, h * 0.5, d * 0.5);
double d_box = q.cwiseMax(0.0).norm()
+ std::min(std::max({q.x(), q.y(), q.z()}), 0.0);
// Diagonal plane x - z ≤ 0 (i.e., z ≥ x)
double d_cut = (p.x() - p.z()) * 0.707106781186548; // 1/√2
return std::max(d_box, d_cut);
}
// ═══════════════════════════════════════════════
// 2D Extrusions & Revolution
// ═══════════════════════════════════════════════
/**
* @brief 二维 SDF 沿 Z 轴的无限挤出
*
* 将二维 SDF 沿 Z 轴无限延伸。三维点的 SDF 值等于其 (x,y) 投影处的二维 SDF 值。
*
* **使用模式:** 先计算二维 SDF 值,再传入此函数:
*
* @code{.cpp}
* double sdf_2d = circle_sdf(p.x(), p.y()); // 用户自定义二维 SDF
* double sdf_3d = extrusion(p, sdf_2d);
* @endcode
*
* @param p 三维采样点(仅 x,y 分量被隐式使用——通过 sdf_2d 参数间接)
* @param sdf_2d 预计算的二维 SDF 值(在 (p.x, p.y) 处求值)
* @return 三维有符号距离(等于 sdf_2d)
*
* @note p 参数仅保持接口一致,实际不影响结果。
* @note 对于有限长度的挤出,使用 extrusion_bounded()。
*
* @see extrusion_bounded 有限长度的挤出
* @see revolution 二维轮廓绕 Y 轴旋转
*/
[[nodiscard]] inline double extrusion(const Point3D& /*p*/, double sdf_2d) {
return sdf_2d;
}
/**
* @brief 二维 SDF 的有限长度挤出
*
* 将二维 SDF 沿 Z 轴挤出并通过 |pz| ≤ half_height 做 bounded 限制。
* 相当于对二维 SDF 与 Z 轴平板做交集。
*
* @param p 三维采样点
* @param sdf_2d 预计算的二维 SDF 值(在 (p.x, p.y) 处求值)
* @param half_height 挤出半高度(Z 方向),必须 ≥ 0
* @return 有符号距离
*
* @code{.cpp}
* // 圆形截面、总高 4.0 的短柱
* double d2d = std::sqrt(p.x()*p.x() + p.y()*p.y()) - 1.0;
* double d3d = extrusion_bounded(p, d2d, 2.0);
* @endcode
*
* @see extrusion 无限挤出
* @see hex_prism 六棱柱(特例化 bounded extrusion 实现)
*/
[[nodiscard]] inline double extrusion_bounded(const Point3D& p, double sdf_2d, double half_height) {
return std::max(sdf_2d, std::abs(p.z()) - half_height);
}
/**
* @brief 二维轮廓绕 Y 轴的旋转体 SDF
*
* 将二维 SDF 绕 Y 轴旋转,生成旋转体的三维 SDF。
*
* **使用模式:**
* 二维 SDF 定义在 (r, y) 坐标中,其中 r 为水平距离 = √(pₓ² + p𝓏²) - offset。
* 用户需要在调用前将 (r, y) 转换为二维采样并计算 SDF 值。
*
* @param p 三维采样点(接口保留,不直接使用)
* @param sdf_2d 预计算的二维 SDF(在 (√(pₓ²+p𝓏²)-offset, p.y) 处求值)
* @param offset 径向偏移量(从 Y 轴的起点距离)
* @return 有符号距离
*
* @code{.cpp}
* // 在距 Y 轴 2.0 处、截面半径 0.5 的圆环
* double r = std::sqrt(p.x()*p.x() + p.z()*p.z()) - 2.0;
* double sdf_2d = std::sqrt(r*r + p.y()*p.y()) - 0.5;
* double d3d = revolution(p, sdf_2d, 2.0);
* @endcode
*
* @see torus 圆环(revolution 的特例:截面为圆)
*/
[[nodiscard]] inline double revolution(const Point3D& /*p*/, double sdf_2d, double /*offset*/) {
return sdf_2d;
}
// ═══════════════════════════════════════════════
// Complex Primitives (implemented in src/sdf/)
// ═══════════════════════════════════════════════
/**
* @brief 圆锥台的 SDF(非内联实现)
*
* 沿 Y 轴、以原点为中心的有限长圆锥台。
* 顶点在 y = +height/2(半径 0),底面在 y = -height/2(半径 height·tan(angle_rad))。
*
* **算法概要:**
* 将三维问题分解为二维 (r, y) 平面内点到三角形区域的距离计算,
* 其中 r = √(pₓ² + p𝓏²),三角形由底边和高决定。
*
* @param p 采样点
* @param angle_rad 半锥角(弧度),决定底面半径
* @param height 锥体总高度,必须 > 0
* @return 有符号距离
*
* @warning 当 angle_rad 接近 π/2 时底面半径极大,可能导致数值不稳定。
*
* @see cylinder 半锥角为 0 时的退化情况
* @see infinite_cone 无限圆锥
*/
[[nodiscard]] double cone(const Point3D& p, double angle_rad, double height);
/**
* @brief 三角棱柱的 SDF(非内联实现)
*
* 以 XY 平面中任意三角形为截面、沿 Z 轴 ±height/2 挤出的棱柱。
*
* @param p 采样点
* @param a 三角形顶点 AXY 平面内)
* @param b 三角形顶点 BXY 平面内)
* @param c 三角形顶点 CXY 平面内)
* @param height 挤出总高度(沿 Z 轴),必须 > 0
* @return 有符号距离
*
* @pre a, b, c 三点不共线(构成有效三角形)
*
* @code{.cpp}
* // 顶点为 (0,0), (2,0), (0,2)、高度 1.0 的三棱柱
* double d = triangular_prism(p, Point3D(0,0,0), Point3D(2,0,0), Point3D(0,2,0), 1.0);
* @endcode
*
* @see wedge 直角三角形楔形体
*/
[[nodiscard]] double triangular_prism(const Point3D& p, const Point3D& a, const Point3D& b,
const Point3D& c, double height);
/**
* @brief 双环链接体的 SDF(非内联实现)
*
* 两个并列的圆环面沿 X 轴排列,等效于经典链环结构。
* 常用于链条、连杆等机械连接件的快速建模。
*
* @param p 采样点
* @param length 两个环心沿 X 轴的距离
* @param major_r 每个环的主半径(环的半径)
* @param minor_r 每个环的管半径(截面的粗细)
* @return 有符号距离
*
* @code{.cpp}
* // 环间距 2.0、主半径 1.0、管半径 0.2 的双环
* double d = link(Point3D(1.0, 0.0, 0.0), 2.0, 1.0, 0.2);
* @endcode
*
* @see torus 单个圆环
*/
[[nodiscard]] double link(const Point3D& p, double length, double major_r, double minor_r);
/**
* @brief 无限圆锥的 SDF(非内联实现)
*
* 从顶点出发沿轴方向无限延伸的圆锥,半锥角为 angle_rad。
* 与 cone() 不同,本函数无高度限制,锥体向轴的正方向无限扩展。
*
* @param p 采样点
* @param apex 锥顶位置
* @param axis 锥轴方向(必须归一化),锥体沿此方向扩展
* @param angle_rad 半锥角(弧度)
* @return 有符号距离
*
* @pre axis.norm() ≈ 1.0
*
* @code{.cpp}
* // 顶点在原点、沿 Y 轴、半锥角 30° 的无限锥
* double d = infinite_cone(p, Point3D(0,0,0), Vector3D(0,1,0), M_PI/6);
* @endcode
*
* @see cone 有限高圆锥台
*/
[[nodiscard]] double infinite_cone(const Point3D& p, const Point3D& apex,
const Vector3D& axis, double angle_rad);
} // namespace vde::sdf