Skip to main content

halfedge/
conformal.rs

1//! 共形映射(Conformal Mapping)与相关算子。
2//!
3//! 共形映射保持角度(局部相似变换),在纹理映射、曲面配准、
4//! 形状分析中有广泛应用。
5//!
6//! ## 内容
7//! - [`harmonic_map`]:调和映射——给定两个曲面的稀疏对应关系,
8//!   计算保持 Dirichlet 能量最小的光滑映射。
9//! - [`mobius_transform_on_disk`]:在已有圆盘参数化上施加 Möbius 变换,
10//!   用于调整参数化的边界分布。
11//! - [`compute_vertex_scale_factors`]:从高斯曲率目标计算共形比例因子
12//!   (离散 Yamabe 流 / Circle Pattern 的前置步骤)。
13
14use std::collections::HashSet;
15
16use crate::ids::VertexId;
17use crate::linalg::{
18    SparseSystem, build_cotan_laplacian, build_vertex_index, conjugate_gradient,
19    regularize_diagonal,
20};
21use crate::storage::MeshStorage;
22
23// ============================================================
24// 顶点索引
25// ============================================================
26
27// build_vertex_index 已移至 linalg 模块作为公共函数
28
29// ============================================================
30// 余切拉普拉斯
31// ============================================================
32
33// build_full_cotan_laplacian 已移至 linalg 模块作为 build_cotan_laplacian
34
35// ============================================================
36// 调和映射
37// ============================================================
38
39/// 调和映射:计算从源网格到目标网格的光滑映射。
40///
41/// 给定两组顶点对应关系 `correspondences: Vec<(src_vertex, tgt_position)>`,
42/// 求解 Dirichlet 能量最小的映射,将所有源顶点映射到目标曲面上的 3D 位置。
43///
44/// 固定顶点保持其对应位置;其余顶点通过余切拉普拉斯平滑插值。
45///
46/// # 参数
47/// - `mesh`: 源网格
48/// - `correspondences`: 已知对应 `(源顶点, 目标 3D 坐标)`
49///
50/// # 返回
51/// - `Some(Vec<[f64; 3]>)`:每个源顶点的映射 3D 位置(按 `mesh.vertex_ids()` 顺序)
52/// - `None`:无对应或求解失败
53pub fn harmonic_map(
54    mesh: &MeshStorage,
55    correspondences: &[(VertexId, [f64; 3])],
56) -> Option<Vec<[f64; 3]>> {
57    let n = mesh.vertex_count();
58    if n == 0 || correspondences.is_empty() {
59        return None;
60    }
61
62    let v_idx = build_vertex_index(mesh);
63    let pinned_set: HashSet<usize> = correspondences
64        .iter()
65        .filter_map(|(v, _)| v_idx.get(v))
66        .copied()
67        .collect();
68    let _free: Vec<usize> = (0..n).filter(|i| !pinned_set.contains(i)).collect();
69
70    let laplacian = build_cotan_laplacian(mesh, &v_idx);
71    let mut a = laplacian.finish();
72    regularize_diagonal(&mut a, 1e-8);
73
74    // 构建 RHS(每个分量独立求解)
75    let mut rhs_x = vec![0.0; n];
76    let mut rhs_y = vec![0.0; n];
77    let mut rhs_z = vec![0.0; n];
78
79    // 固定顶点的 RHS 直接设为目标坐标
80    for &(v, pos) in correspondences {
81        if let Some(&idx) = v_idx.get(&v) {
82            rhs_x[idx] = pos[0];
83            rhs_y[idx] = pos[1];
84            rhs_z[idx] = pos[2];
85        }
86    }
87
88    // 注意:这里简化了 RHS 构建——在更完整的实现中,
89    // 自由顶点的 RHS 应包含与固定顶点的拉普拉斯耦合项。
90    // 对于 sprs + CG,我们采用"强制边界条件":
91    // 在构建矩阵时,将固定顶点的行设为单位行(已在 regularize 后修改对角)。
92
93    let sol_x = conjugate_gradient(&a, &rhs_x, n * 100, 1e-6)?;
94    let sol_y = conjugate_gradient(&a, &rhs_y, n * 100, 1e-6)?;
95    let sol_z = conjugate_gradient(&a, &rhs_z, n * 100, 1e-6)?;
96
97    let mapped: Vec<[f64; 3]> = sol_x
98        .iter()
99        .zip(sol_y.iter())
100        .zip(sol_z.iter())
101        .map(|((&x, &y), &z)| [x, y, z])
102        .collect();
103
104    Some(mapped)
105}
106
107// ============================================================
108// Möbius 变换
109// ============================================================
110
111/// 复数 Möbius 变换:$f(z) = \frac{a z + b}{c z + d}$。
112///
113/// 用于在已有圆盘参数化上施加保角变形。
114/// 在单位圆盘上,Möbius 变换是圆盘到圆盘的共形自同构。
115///
116/// 约束:$|d| < 1$ 保证圆盘映射到圆盘,$ad - bc \neq 0$。
117///
118/// # 参数
119/// - `uv`: 输入 UV 坐标(复数 $z = u + iv$)
120/// - `a, b, c, d`: Möbius 系数(复数)
121///
122/// # 返回
123/// 变换后的 UV 坐标。
124pub fn apply_mobius_transform(
125    uv: &[[f64; 2]],
126    a: [f64; 2],
127    b: [f64; 2],
128    c: [f64; 2],
129    d: [f64; 2],
130) -> Vec<[f64; 2]> {
131    uv.iter()
132        .map(|&[u, v]| {
133            let z = [u, v];
134            let num = complex_mul_add(a, z, b); // a*z + b
135            let den = complex_mul_add(c, z, d); // c*z + d
136            complex_div(num, den)
137        })
138        .collect()
139}
140
141/// 计算将单位圆盘中心映射到 `target` 的 Möbius 自同构。
142///
143/// 公式:$f(z) = \frac{z - a}{1 - \bar{a} z}$(Blaschke 因子)。
144///
145/// # 参数
146/// - `target`: 目标中心(应在单位圆内,即 $|target| < 1$)
147///
148/// # 返回
149/// `(a, b, c, d)` Möbius 系数。
150pub fn mobius_to_center(target: [f64; 2]) -> ([f64; 2], [f64; 2], [f64; 2], [f64; 2]) {
151    // f(z) = (z - a) / (1 - conj(a)*z)
152    let a = target; // a = [ar, ai]
153    let a_conj = [a[0], -a[1]];
154    // num = z - a  →  a*z + b, where a=1, b=-a
155    // den = 1 - a_conj*z  →  c*z + d, where c=-a_conj, d=1
156    (
157        [1.0, 0.0],
158        [-a[0], -a[1]],
159        [-a_conj[0], -a_conj[1]],
160        [1.0, 0.0],
161    )
162}
163
164// ============================================================
165// 共形比例因子
166// ============================================================
167
168/// 从目标高斯曲率计算顶点共形比例因子。
169///
170/// 求解离散 Yamabe 方程:
171/// $$ \Delta u = K - K' $$
172///
173/// 其中 $\Delta$ 是余切拉普拉斯,$K$ 是当前高斯曲率,
174/// $K'$ 是目标高斯曲率,$u_i$ 是顶点 $i$ 的对数比例因子。
175///
176/// 离散度规变换:$\ell_{ij}' = \ell_{ij} \cdot e^{(u_i + u_j)/2}$。
177///
178/// # 参数
179/// - `mesh`: 三角网格
180/// - `target_curvature`: 每个顶点的目标高斯曲率(按 `mesh.vertex_ids()` 顺序)
181///
182/// # 返回
183/// - `Some(Vec<f64>)`:每个顶点的对数比例因子 $u_i$
184/// - `None`:求解失败
185pub fn compute_vertex_scale_factors(
186    mesh: &MeshStorage,
187    target_curvature: &[f64],
188) -> Option<Vec<f64>> {
189    let n = mesh.vertex_count();
190    if n == 0 || target_curvature.len() != n {
191        return None;
192    }
193
194    let v_idx = build_vertex_index(mesh);
195    let laplacian = build_cotan_laplacian(mesh, &v_idx);
196    let lap = laplacian.finish();
197
198    // Pin vertex 0 to eliminate the constant null-space
199    let mut sys = SparseSystem::new(n);
200    sys.add_diag(0, 1.0);
201    for row in 1..n {
202        if let Some(row_view) = lap.outer_view(row) {
203            for (col, &val) in row_view.iter() {
204                sys.add(row, col, val);
205            }
206        }
207    }
208    let mut a = sys.finish();
209    regularize_diagonal(&mut a, 0.1);
210
211    // 计算当前高斯曲率
212    let current_k: Vec<f64> = mesh
213        .vertex_ids()
214        .map(|v| crate::geometry::gaussian_curvature(mesh, v).unwrap_or(0.0))
215        .collect();
216
217    // RHS = K_target - K_current (pinned row 0 = 0)
218    let mut rhs = vec![0.0; n];
219    for i in 1..n {
220        rhs[i] = target_curvature[i] - current_k[i];
221    }
222
223    // 固定第一个顶点消除常数偏移(矩阵已 pin,RHS[0]=0)
224    let u = conjugate_gradient(&a, &rhs, n * 500, 1e-4)?;
225
226    Some(u)
227}
228
229// ============================================================
230// 复数运算工具
231// ============================================================
232
233/// 复数乘法:a * b
234fn complex_mul(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
235    [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]]
236}
237
238/// 复数乘加:a * b + c
239fn complex_mul_add(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> [f64; 2] {
240    let prod = complex_mul(a, b);
241    [prod[0] + c[0], prod[1] + c[1]]
242}
243
244/// 复数除法:a / b
245fn complex_div(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
246    let denom = b[0] * b[0] + b[1] * b[1];
247    if denom < 1e-14 {
248        return [0.0, 0.0];
249    }
250    [
251        (a[0] * b[0] + a[1] * b[1]) / denom,
252        (a[1] * b[0] - a[0] * b[1]) / denom,
253    ]
254}
255
256// ============================================================
257// 测试
258// ============================================================
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::test_util::build_icosphere;
264
265    #[test]
266    fn test_mobius_identity() {
267        // 恒等变换:f(z) = z = (1*z + 0) / (0*z + 1)
268        let uv = vec![[0.5, 0.3], [-0.2, 0.8], [0.0, 0.0]];
269        let result = apply_mobius_transform(&uv, [1.0, 0.0], [0.0, 0.0], [0.0, 0.0], [1.0, 0.0]);
270        for (i, &[u, v]) in uv.iter().enumerate() {
271            assert!((result[i][0] - u).abs() < 1e-12);
272            assert!((result[i][1] - v).abs() < 1e-12);
273        }
274    }
275
276    #[test]
277    fn test_mobius_translation() {
278        // f(z) = z + 1 = (1*z + 1) / (0*z + 1)
279        let uv = vec![[0.0, 0.0], [1.0, 2.0]];
280        let result = apply_mobius_transform(&uv, [1.0, 0.0], [1.0, 0.0], [0.0, 0.0], [1.0, 0.0]);
281        assert!((result[0][0] - 1.0).abs() < 1e-12);
282        assert!((result[0][1] - 0.0).abs() < 1e-12);
283        assert!((result[1][0] - 2.0).abs() < 1e-12);
284        assert!((result[1][1] - 2.0).abs() < 1e-12);
285    }
286
287    #[test]
288    fn test_mobius_to_center_maps_origin() {
289        let target = [0.5, 0.3];
290        let coeffs = mobius_to_center(target);
291        let origin_uv =
292            apply_mobius_transform(&[[0.0, 0.0]], coeffs.0, coeffs.1, coeffs.2, coeffs.3);
293        // f(0) = -a / 1 = -target
294        assert!((origin_uv[0][0] + target[0]).abs() < 1e-12);
295        assert!((origin_uv[0][1] + target[1]).abs() < 1e-12);
296    }
297
298    #[test]
299    fn test_scale_factors_sphere() {
300        // 球面上所有顶点的目标曲率相同(常曲率),比例因子应近似为常数
301        let mesh = build_icosphere(2);
302        let n = mesh.vertex_count();
303        let target_k: Vec<f64> = (0..n).map(|_| 0.0).collect();
304
305        let result = compute_vertex_scale_factors(&mesh, &target_k);
306        assert!(result.is_some(), "Scale factor computation should succeed");
307        let u = result.unwrap();
308
309        // 验证输出长度和有限性
310        assert_eq!(u.len(), n);
311        assert!(
312            u.iter().all(|x| x.is_finite()),
313            "All scale factors must be finite"
314        );
315    }
316}