feat(sdf): S12 — 可微分几何完整模块
S12-A: 自动微分引擎 - sdf_gradient.h: 前向/反向梯度、解析梯度(sphere/box/torus/cylinder/plane/capsule) - 链式法则: union/intersection/difference/smooth + translate/rotate/scale/twist/repeat - 参数梯度: sphere/box/cylinder 的 param_grad - test_sdf_gradient.cpp: 604 行测试 S12-B: 梯度驱动优化 + 应用 - sdf_optimize.h: 形状拟合/碰撞避免/可达性/对称检测/体积计算 - sdf_optimize.cpp: 682 行实现(数值梯度 + GradientDescent) - test_sdf_optimize.cpp: 342 行,20 项测试 S12-C: PyTorch 集成 + Python - sdf_torch.h/cpp: 批量 SDF 求值 + 梯度 - python/vde/sdf.py: 高级 Python API - python/vde/torch_sdf.py: torch.autograd.Function - test_sdf_torch_bridge.cpp: C++ 桥梁测试
This commit is contained in:
@@ -172,6 +172,7 @@ add_library(vde_sdf STATIC
|
||||
sdf/sdf_tree.cpp
|
||||
sdf/sdf_to_mesh.cpp
|
||||
sdf/sdf_torch.cpp
|
||||
sdf/sdf_optimize.cpp
|
||||
)
|
||||
target_include_directories(vde_sdf
|
||||
PUBLIC ${CMAKE_SOURCE_DIR}/include
|
||||
|
||||
@@ -0,0 +1,682 @@
|
||||
#include "vde/sdf/sdf_optimize.h"
|
||||
#include "vde/sdf/sdf_gradient.h"
|
||||
#include "vde/sdf/sdf_primitives.h"
|
||||
#include "vde/sdf/sdf_operations.h"
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vde::sdf {
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Internal helpers
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
namespace {
|
||||
|
||||
/// Thread-local RNG
|
||||
std::mt19937& rng() {
|
||||
thread_local std::mt19937 gen(std::random_device{}());
|
||||
return gen;
|
||||
}
|
||||
|
||||
/// Generate a uniform random double in [lo, hi]
|
||||
double rand_double(double lo, double hi) {
|
||||
return std::uniform_real_distribution<double>(lo, hi)(rng());
|
||||
}
|
||||
|
||||
/// Reflect point p across plane defined by normal n (assumed unit) and offset d.
|
||||
/// reflected = p - 2 * (n·p - d) * n
|
||||
Point3D reflect_point(const Point3D& p, const Vector3D& normal, double offset) {
|
||||
double dist = p.dot(normal) - offset;
|
||||
return p - normal * (2.0 * dist);
|
||||
}
|
||||
|
||||
/// Generate N random points in axis-aligned bounding box
|
||||
std::vector<Point3D> sample_bbox(const Point3D& bmin, const Point3D& bmax, int n) {
|
||||
std::vector<Point3D> pts;
|
||||
pts.reserve(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
pts.emplace_back(rand_double(bmin.x(), bmax.x()),
|
||||
rand_double(bmin.y(), bmax.y()),
|
||||
rand_double(bmin.z(), bmax.z()));
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
/// Create a copy of the SDF tree (deep copy)
|
||||
SdfNodePtr deep_copy(const SdfNodePtr& node) {
|
||||
if (!node) return nullptr;
|
||||
auto copy = std::make_shared<SdfNode>(node->op);
|
||||
copy->params = node->params;
|
||||
copy->name = node->name;
|
||||
for (const auto& child : node->children) {
|
||||
copy->children.push_back(deep_copy(child));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Check if two SDF shapes overlap (any penetration)
|
||||
bool shapes_overlap(const SdfNodePtr& a, const SdfNodePtr& b,
|
||||
const Point3D& bmin, const Point3D& bmax,
|
||||
int samples) {
|
||||
auto pts = sample_bbox(bmin, bmax, samples);
|
||||
for (const auto& p : pts) {
|
||||
if (evaluate(a, p) < 0.0 && evaluate(b, p) < 0.0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Parameter Collection & Numerical Gradient
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
std::vector<ParamRef> collect_params(SdfNodePtr& root) {
|
||||
std::vector<ParamRef> params;
|
||||
if (!root) return params;
|
||||
|
||||
root->visit([&](SdfNode& node) {
|
||||
// Only collect parameters from primitive leaf nodes (no children)
|
||||
if (!node.children.empty()) return;
|
||||
|
||||
switch (node.op) {
|
||||
case SdfOp::Sphere:
|
||||
params.push_back({&node.params.radius, "radius"});
|
||||
break;
|
||||
case SdfOp::Box:
|
||||
params.push_back({&node.params.extents.x(), "extent_x"});
|
||||
params.push_back({&node.params.extents.y(), "extent_y"});
|
||||
params.push_back({&node.params.extents.z(), "extent_z"});
|
||||
break;
|
||||
case SdfOp::RoundBox:
|
||||
params.push_back({&node.params.extents.x(), "extent_x"});
|
||||
params.push_back({&node.params.extents.y(), "extent_y"});
|
||||
params.push_back({&node.params.extents.z(), "extent_z"});
|
||||
params.push_back({&node.params.radius, "rounding"});
|
||||
break;
|
||||
case SdfOp::Cylinder:
|
||||
params.push_back({&node.params.radius, "radius"});
|
||||
params.push_back({&node.params.height, "height"});
|
||||
break;
|
||||
case SdfOp::Torus:
|
||||
params.push_back({&node.params.major_radius, "major_r"});
|
||||
params.push_back({&node.params.minor_radius, "minor_r"});
|
||||
break;
|
||||
case SdfOp::Capsule:
|
||||
params.push_back({&node.params.pt_a.x(), "pt_a_x"});
|
||||
params.push_back({&node.params.pt_a.y(), "pt_a_y"});
|
||||
params.push_back({&node.params.pt_a.z(), "pt_a_z"});
|
||||
params.push_back({&node.params.pt_b.x(), "pt_b_x"});
|
||||
params.push_back({&node.params.pt_b.y(), "pt_b_y"});
|
||||
params.push_back({&node.params.pt_b.z(), "pt_b_z"});
|
||||
params.push_back({&node.params.radius, "radius"});
|
||||
break;
|
||||
case SdfOp::Cone:
|
||||
params.push_back({&node.params.angle_rad, "angle_rad"});
|
||||
params.push_back({&node.params.height, "height"});
|
||||
break;
|
||||
case SdfOp::Ellipsoid:
|
||||
params.push_back({&node.params.extents.x(), "radius_x"});
|
||||
params.push_back({&node.params.extents.y(), "radius_y"});
|
||||
params.push_back({&node.params.extents.z(), "radius_z"});
|
||||
break;
|
||||
case SdfOp::HexPrism:
|
||||
params.push_back({&node.params.radius, "radius"});
|
||||
params.push_back({&node.params.height, "height"});
|
||||
break;
|
||||
case SdfOp::TriangularPrism:
|
||||
params.push_back({&node.params.height, "height"});
|
||||
break;
|
||||
case SdfOp::Wedge:
|
||||
params.push_back({&node.params.extents.x(), "extent_x"});
|
||||
params.push_back({&node.params.extents.y(), "extent_y"});
|
||||
params.push_back({&node.params.extents.z(), "extent_z"});
|
||||
break;
|
||||
case SdfOp::Link:
|
||||
params.push_back({&node.params.radius, "major_r"});
|
||||
params.push_back({&node.params.height, "length"});
|
||||
params.push_back({&node.params.thickness, "thickness"});
|
||||
break;
|
||||
case SdfOp::Plane:
|
||||
params.push_back({&node.params.normal.x(), "normal_x"});
|
||||
params.push_back({&node.params.normal.y(), "normal_y"});
|
||||
params.push_back({&node.params.normal.z(), "normal_z"});
|
||||
params.push_back({&node.params.offset, "offset"});
|
||||
break;
|
||||
case SdfOp::Translate:
|
||||
params.push_back({&node.params.translate_offset.x(), "tx"});
|
||||
params.push_back({&node.params.translate_offset.y(), "ty"});
|
||||
params.push_back({&node.params.translate_offset.z(), "tz"});
|
||||
break;
|
||||
case SdfOp::Scale:
|
||||
params.push_back({&node.params.scale_factors.x(), "sx"});
|
||||
params.push_back({&node.params.scale_factors.y(), "sy"});
|
||||
params.push_back({&node.params.scale_factors.z(), "sz"});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
// Other ops don't have optimizable parameters or are non-primitive
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
std::vector<double> numerical_gradient(
|
||||
const std::vector<ParamRef>& params,
|
||||
const std::function<double()>& loss_fn,
|
||||
double eps) {
|
||||
|
||||
std::vector<double> grad(params.size(), 0.0);
|
||||
double base_loss = loss_fn();
|
||||
|
||||
for (size_t i = 0; i < params.size(); ++i) {
|
||||
if (!params[i].value) continue;
|
||||
double orig = *params[i].value;
|
||||
*params[i].value = orig + eps;
|
||||
double loss_plus = loss_fn();
|
||||
*params[i].value = orig; // restore
|
||||
grad[i] = (loss_plus - base_loss) / eps;
|
||||
}
|
||||
|
||||
return grad;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Shape Fitting
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
OptimizeResult fit_to_point_cloud(
|
||||
const SdfNodePtr& initial,
|
||||
const std::vector<Point3D>& target_points,
|
||||
double learning_rate,
|
||||
int max_iterations) {
|
||||
|
||||
// Work on a copy so the caller's tree isn't modified
|
||||
auto shape = deep_copy(initial);
|
||||
auto params = collect_params(shape);
|
||||
|
||||
OptimizeResult result;
|
||||
result.optimized_shape = shape;
|
||||
result.iterations = 0;
|
||||
result.converged = false;
|
||||
|
||||
if (target_points.empty() || params.empty()) {
|
||||
result.final_loss = 0.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
const double loss_threshold = 1e-4;
|
||||
|
||||
// Loss: mean absolute SDF value at target points
|
||||
auto compute_loss = [&]() -> double {
|
||||
double sum = 0.0;
|
||||
for (const auto& p : target_points) {
|
||||
sum += std::abs(evaluate(shape, p));
|
||||
}
|
||||
return sum / target_points.size();
|
||||
};
|
||||
|
||||
for (int iter = 0; iter < max_iterations; ++iter) {
|
||||
double loss = compute_loss();
|
||||
result.loss_history.push_back(loss);
|
||||
result.iterations = iter + 1;
|
||||
|
||||
if (loss < loss_threshold) {
|
||||
result.converged = true;
|
||||
result.final_loss = loss;
|
||||
return result;
|
||||
}
|
||||
|
||||
auto grad = numerical_gradient(params, compute_loss);
|
||||
for (size_t i = 0; i < params.size(); ++i) {
|
||||
if (params[i].value) {
|
||||
*params[i].value -= learning_rate * grad[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp parameters to sensible ranges
|
||||
for (auto& p : params) {
|
||||
if (p.value) {
|
||||
// Prevent negative sizes
|
||||
if (*p.value < 1e-6) *p.value = 1e-6;
|
||||
// Prevent unreasonably large
|
||||
if (*p.value > 1e6) *p.value = 1e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.final_loss = compute_loss();
|
||||
return result;
|
||||
}
|
||||
|
||||
OptimizeResult fit_surface_to_points(
|
||||
const SdfNodePtr& initial,
|
||||
const std::vector<Point3D>& surface_points,
|
||||
double learning_rate,
|
||||
int max_iterations) {
|
||||
return fit_to_point_cloud(initial, surface_points, learning_rate, max_iterations);
|
||||
}
|
||||
|
||||
OptimizeResult fit_to_sdf(
|
||||
const SdfNodePtr& source,
|
||||
const std::function<double(const Point3D&)>& target_sdf,
|
||||
const Point3D& bmin, const Point3D& bmax,
|
||||
int grid_resolution,
|
||||
double learning_rate,
|
||||
int max_iterations) {
|
||||
|
||||
auto shape = deep_copy(source);
|
||||
auto params = collect_params(shape);
|
||||
|
||||
OptimizeResult result;
|
||||
result.optimized_shape = shape;
|
||||
result.iterations = 0;
|
||||
result.converged = false;
|
||||
|
||||
if (params.empty()) {
|
||||
result.final_loss = 0.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Build grid of sample points
|
||||
std::vector<Point3D> sample_points;
|
||||
sample_points.reserve(grid_resolution * grid_resolution * grid_resolution);
|
||||
for (int ix = 0; ix < grid_resolution; ++ix) {
|
||||
double tz = (grid_resolution > 1) ? ix / double(grid_resolution - 1) : 0.5;
|
||||
double x = bmin.x() + tz * (bmax.x() - bmin.x());
|
||||
for (int iy = 0; iy < grid_resolution; ++iy) {
|
||||
double ty = (grid_resolution > 1) ? iy / double(grid_resolution - 1) : 0.5;
|
||||
double y = bmin.y() + ty * (bmax.y() - bmin.y());
|
||||
for (int iz = 0; iz < grid_resolution; ++iz) {
|
||||
double tz2 = (grid_resolution > 1) ? iz / double(grid_resolution - 1) : 0.5;
|
||||
double z = bmin.z() + tz2 * (bmax.z() - bmin.z());
|
||||
sample_points.emplace_back(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Precompute target SDF values at grid points
|
||||
std::vector<double> target_vals;
|
||||
target_vals.reserve(sample_points.size());
|
||||
for (const auto& p : sample_points) {
|
||||
target_vals.push_back(target_sdf(p));
|
||||
}
|
||||
|
||||
const double loss_threshold = 1e-5;
|
||||
|
||||
auto compute_loss = [&]() -> double {
|
||||
double sum = 0.0;
|
||||
for (size_t i = 0; i < sample_points.size(); ++i) {
|
||||
double diff = evaluate(shape, sample_points[i]) - target_vals[i];
|
||||
sum += diff * diff;
|
||||
}
|
||||
return sum / sample_points.size();
|
||||
};
|
||||
|
||||
for (int iter = 0; iter < max_iterations; ++iter) {
|
||||
double loss = compute_loss();
|
||||
result.loss_history.push_back(loss);
|
||||
result.iterations = iter + 1;
|
||||
|
||||
if (loss < loss_threshold) {
|
||||
result.converged = true;
|
||||
result.final_loss = loss;
|
||||
return result;
|
||||
}
|
||||
|
||||
auto grad = numerical_gradient(params, compute_loss);
|
||||
for (size_t i = 0; i < params.size(); ++i) {
|
||||
if (params[i].value) {
|
||||
*params[i].value -= learning_rate * grad[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& p : params) {
|
||||
if (p.value) {
|
||||
if (*p.value < 1e-6) *p.value = 1e-6;
|
||||
if (*p.value > 1e6) *p.value = 1e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.final_loss = compute_loss();
|
||||
return result;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Collision Avoidance
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
bool resolve_collision(
|
||||
SdfNodePtr& shape_a, SdfNodePtr& shape_b,
|
||||
double step_size,
|
||||
int max_iterations) {
|
||||
|
||||
// Estimate bounding box containing both shapes
|
||||
double ra = estimate_bounds(shape_a).norm();
|
||||
double rb = estimate_bounds(shape_b).norm();
|
||||
double r = std::max(ra, rb) * 1.5;
|
||||
Point3D bmin(-r, -r, -r);
|
||||
Point3D bmax(r, r, r);
|
||||
|
||||
const int check_samples = 500;
|
||||
|
||||
for (int iter = 0; iter < max_iterations; ++iter) {
|
||||
if (!shapes_overlap(shape_a, shape_b, bmin, bmax, check_samples)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find penetration direction: for randomly sampled interior points
|
||||
// of both shapes, compute the average gradient direction pointing
|
||||
// from shape_b interior toward shape_a exterior.
|
||||
Vector3D push_dir = Vector3D::Zero();
|
||||
int count = 0;
|
||||
|
||||
auto pts = sample_bbox(bmin, bmax, check_samples);
|
||||
for (const auto& p : pts) {
|
||||
double da = evaluate(shape_a, p);
|
||||
double db = evaluate(shape_b, p);
|
||||
if (da < 0.0 && db < 0.0) {
|
||||
// Point is inside both — compute SDF gradient of shape_a as push direction
|
||||
auto sdf_a_fn = [&](const Point3D& pt) { return evaluate(shape_a, pt); };
|
||||
Vector3D grad_a = gradient(sdf_a_fn, p);
|
||||
|
||||
double grad_norm = grad_a.norm();
|
||||
if (grad_norm > 1e-8) {
|
||||
push_dir += grad_a.normalized();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
// No overlapping points found or gradient undefined
|
||||
break;
|
||||
}
|
||||
|
||||
push_dir /= count;
|
||||
|
||||
// Translate shape_a along push direction
|
||||
// Find existing translate nodes, or wrap shape_a in a translate
|
||||
// Strategy: wrap shape_a in a Translate node
|
||||
auto translate_node = std::make_shared<SdfNode>(SdfOp::Translate);
|
||||
translate_node->params.translate_offset = push_dir * step_size;
|
||||
translate_node->name = "collision_resolve";
|
||||
translate_node->children = {shape_a};
|
||||
shape_a = translate_node;
|
||||
}
|
||||
|
||||
// Check final state
|
||||
return !shapes_overlap(shape_a, shape_b, bmin, bmax, check_samples);
|
||||
}
|
||||
|
||||
double penetration_depth(
|
||||
const SdfNodePtr& shape_a,
|
||||
const SdfNodePtr& shape_b,
|
||||
const Point3D& bmin, const Point3D& bmax,
|
||||
int samples) {
|
||||
|
||||
auto pts = sample_bbox(bmin, bmax, samples);
|
||||
double max_depth = 0.0;
|
||||
|
||||
for (const auto& p : pts) {
|
||||
double da = evaluate(shape_a, p);
|
||||
double db = evaluate(shape_b, p);
|
||||
if (da < 0.0 && db < 0.0) {
|
||||
// Point is inside both; penetration depth is the smaller
|
||||
// of |da| and |db| (distance to nearest surface)
|
||||
double depth = std::min(std::abs(da), std::abs(db));
|
||||
if (depth > max_depth) {
|
||||
max_depth = depth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return max_depth;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Accessibility
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
double accessibility(
|
||||
const SdfNodePtr& shape,
|
||||
const Point3D& p,
|
||||
const Vector3D& direction,
|
||||
int hemisphere_samples) {
|
||||
|
||||
// Normalize approach direction
|
||||
Vector3D dir = direction.normalized();
|
||||
|
||||
// Build a coordinate frame with dir as the "up" axis
|
||||
Vector3D u, v;
|
||||
if (std::abs(dir.x()) > 0.9) {
|
||||
u = Vector3D(0, 1, 0).cross(dir).normalized();
|
||||
} else {
|
||||
u = Vector3D(1, 0, 0).cross(dir).normalized();
|
||||
}
|
||||
v = dir.cross(u).normalized();
|
||||
|
||||
// Sample hemisphere directions
|
||||
int unoccluded = 0;
|
||||
|
||||
for (int i = 0; i < hemisphere_samples; ++i) {
|
||||
// Cosine-weighted hemisphere sampling
|
||||
double r1 = rand_double(0.0, 1.0);
|
||||
double r2 = rand_double(0.0, 1.0);
|
||||
double phi = 2.0 * M_PI * r1;
|
||||
double cos_theta = std::sqrt(1.0 - r2); // cos(θ) where θ ∈ [0, π/2]
|
||||
double sin_theta = std::sqrt(r2);
|
||||
|
||||
Vector3D ray_dir = dir * cos_theta +
|
||||
u * (sin_theta * std::cos(phi)) +
|
||||
v * (sin_theta * std::sin(phi));
|
||||
ray_dir.normalize();
|
||||
|
||||
// March along the ray and check for occlusion
|
||||
bool occluded = false;
|
||||
const double march_step = 0.05;
|
||||
const int max_steps = 200;
|
||||
|
||||
for (int step = 1; step <= max_steps; ++step) {
|
||||
double t = step * march_step;
|
||||
Point3D probe = p + ray_dir * t;
|
||||
|
||||
// If the probe is inside the shape, ray is occluded
|
||||
if (evaluate(shape, probe) < 0.0) {
|
||||
occluded = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// If we've gone far enough, no more occlusion
|
||||
if (t > 20.0) break;
|
||||
}
|
||||
|
||||
if (!occluded) {
|
||||
++unoccluded;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<double>(unoccluded) / hemisphere_samples;
|
||||
}
|
||||
|
||||
Point3D find_accessible_point(
|
||||
const SdfNodePtr& shape,
|
||||
const Vector3D& approach_dir,
|
||||
const Point3D& bmin, const Point3D& bmax,
|
||||
int grid_res) {
|
||||
|
||||
double best_score = -1.0;
|
||||
Point3D best_point = Point3D::Zero();
|
||||
|
||||
for (int ix = 0; ix < grid_res; ++ix) {
|
||||
double tx = (grid_res > 1) ? ix / double(grid_res - 1) : 0.5;
|
||||
double x = bmin.x() + tx * (bmax.x() - bmin.x());
|
||||
for (int iy = 0; iy < grid_res; ++iy) {
|
||||
double ty = (grid_res > 1) ? iy / double(grid_res - 1) : 0.5;
|
||||
double y = bmin.y() + ty * (bmax.y() - bmin.y());
|
||||
for (int iz = 0; iz < grid_res; ++iz) {
|
||||
double tz = (grid_res > 1) ? iz / double(grid_res - 1) : 0.5;
|
||||
double z = bmin.z() + tz * (bmax.z() - bmin.z());
|
||||
Point3D p(x, y, z);
|
||||
|
||||
// Only evaluate points on or near the surface
|
||||
double d = evaluate(shape, p);
|
||||
if (std::abs(d) > 0.1) continue;
|
||||
|
||||
double score = accessibility(shape, p, approach_dir, 32);
|
||||
if (score > best_score) {
|
||||
best_score = score;
|
||||
best_point = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best_point;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Symmetry Detection
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
double symmetry_score(
|
||||
const SdfNodePtr& shape,
|
||||
const Point3D& bmin, const Point3D& bmax,
|
||||
const Vector3D& plane_normal,
|
||||
double plane_offset,
|
||||
int samples) {
|
||||
|
||||
Vector3D n = plane_normal.normalized();
|
||||
auto pts = sample_bbox(bmin, bmax, samples);
|
||||
|
||||
double total_diff = 0.0;
|
||||
double total_abs = 0.0;
|
||||
|
||||
for (const auto& p : pts) {
|
||||
double sdf_p = evaluate(shape, p);
|
||||
Point3D pr = reflect_point(p, n, plane_offset);
|
||||
double sdf_pr = evaluate(shape, pr);
|
||||
|
||||
total_diff += std::abs(sdf_p - sdf_pr);
|
||||
total_abs += std::abs(sdf_p) + 1e-8; // avoid division by zero
|
||||
}
|
||||
|
||||
if (total_abs < 1e-10) return 1.0;
|
||||
|
||||
double score = 1.0 - total_diff / total_abs;
|
||||
return std::max(0.0, score); // clamp to [0, 1]
|
||||
}
|
||||
|
||||
SymmetryResult find_symmetry_plane(
|
||||
const SdfNodePtr& shape,
|
||||
const Point3D& bmin, const Point3D& bmax,
|
||||
int samples) {
|
||||
|
||||
SymmetryResult best;
|
||||
best.score = -1.0;
|
||||
|
||||
// Search over candidate plane normals (axes + diagonals)
|
||||
std::vector<Vector3D> candidates = {
|
||||
Vector3D(1, 0, 0),
|
||||
Vector3D(0, 1, 0),
|
||||
Vector3D(0, 0, 1),
|
||||
Vector3D(1, 1, 0).normalized(),
|
||||
Vector3D(1, 0, 1).normalized(),
|
||||
Vector3D(0, 1, 1).normalized(),
|
||||
Vector3D(1, 1, 1).normalized(),
|
||||
};
|
||||
|
||||
// Search over candidate offsets (center and small offsets)
|
||||
Point3D center = (bmin + bmax) * 0.5;
|
||||
std::vector<double> offsets = {0.0, -0.1, 0.1, -0.5, 0.5};
|
||||
|
||||
for (const auto& n : candidates) {
|
||||
for (double off : offsets) {
|
||||
double score = symmetry_score(shape, bmin, bmax, n, off, samples);
|
||||
if (score > best.score) {
|
||||
best.score = score;
|
||||
best.normal = n;
|
||||
best.offset = off;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Volume & Center of Mass
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
double estimate_volume(
|
||||
const SdfNodePtr& shape,
|
||||
const Point3D& bmin, const Point3D& bmax,
|
||||
int samples) {
|
||||
|
||||
double bbox_volume = (bmax.x() - bmin.x()) *
|
||||
(bmax.y() - bmin.y()) *
|
||||
(bmax.z() - bmin.z());
|
||||
|
||||
auto pts = sample_bbox(bmin, bmax, samples);
|
||||
int inside_count = 0;
|
||||
|
||||
for (const auto& p : pts) {
|
||||
if (evaluate(shape, p) < 0.0) {
|
||||
++inside_count;
|
||||
}
|
||||
}
|
||||
|
||||
return bbox_volume * (static_cast<double>(inside_count) / samples);
|
||||
}
|
||||
|
||||
Point3D center_of_mass(
|
||||
const SdfNodePtr& shape,
|
||||
const Point3D& bmin, const Point3D& bmax,
|
||||
int samples) {
|
||||
|
||||
auto pts = sample_bbox(bmin, bmax, samples);
|
||||
Point3D sum = Point3D::Zero();
|
||||
int inside_count = 0;
|
||||
|
||||
for (const auto& p : pts) {
|
||||
if (evaluate(shape, p) < 0.0) {
|
||||
sum += p;
|
||||
++inside_count;
|
||||
}
|
||||
}
|
||||
|
||||
if (inside_count == 0) {
|
||||
return Point3D::Zero();
|
||||
}
|
||||
|
||||
return sum / inside_count;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// GradientDescent
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
double GradientDescent::step(
|
||||
std::vector<double>& params,
|
||||
const std::function<double(const std::vector<double>&,
|
||||
std::vector<double>&)>& grad_fn) {
|
||||
|
||||
std::vector<double> grad(params.size(), 0.0);
|
||||
double loss = grad_fn(params, grad);
|
||||
|
||||
for (size_t i = 0; i < params.size(); ++i) {
|
||||
params[i] -= lr_ * grad[i];
|
||||
}
|
||||
|
||||
++iteration_;
|
||||
return loss;
|
||||
}
|
||||
|
||||
} // namespace vde::sdf
|
||||
Reference in New Issue
Block a user