Files
openMVS/fill2sc/vacuum_wrap.py
T
2026-11-09 11:42:05 +08:00

1047 lines
46 KiB
Python
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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
vacuum_wrap.py —— 眼镜“抽真空包装” mesh 生成脚本
================================================
参考 youshui_result.obj 中新增的 o Transparent 部分(一个完全包裹眼镜外轮廓、
并填充镜片镂空处的水密闭合 mesh), 本脚本从 youshui.obj + index.txt 出发重新生成它。
输入/输出(自动探测, 也可用 --obj/--index/--out 显式指定):
模型 本目录最大的非输出 *.obj(排除 *result*/*wrap*/*_fill*)
索引 模型同目录的 index.txt(眼镜顶点索引, 每行一个, 0-based)
输出 模型同目录 <模型名>_wrap_result.obj = 原模型 + 追加 o Transparent
处理其它数据集时, 把本脚本拷到该数据目录下直接运行即可。
思路(“抽真空包装”):
1. 由 index.txt 提取眼镜子网格(三个顶点都在索引里的面);
2. 判定正面视线轴后, 只取“朝前的面”(法线与视线轴余弦 > 阈值)投影
栅格化, 形态学闭运算补缝, 取含镜片镂空的最大连通组件,
内部封闭镂空自动填满 → 得到镜框正面“外轮廓”多边形
(镜片镂空被盖住, 鼻梁处的凹口保留; 向后弯折的镜腿被排除,
包装不超出眼镜正面外框);
3. 在外轮廓内铺一层网格点, 作为“包装薄膜”的采样点;
4. 每个网格点从正前方贴到眼镜表面(取该处最靠前的表面高度);
镜片镂空处没有表面 → 用周边镜框高度拟合二次趋势面外推
(跟随镜框倾斜/缓弯, 平齐不下陷), 残差在孔内做调和衔接,
平滑无台阶; footprint 按镜架整体投影剪裁, 不超出镜架;
5. 薄膜沿视线后退 thickness 得到背面膜(最深不超过镜架最后缘), 周边侧壁缝合
→ 一个水密(watertight)闭合的包装 mesh;
6. 棋盘式交替对角线剖分; 薄膜边缘沿边界曲线做一维拉普拉斯收边,
磨掉 footprint 栅格化的锯齿轮廓;
7. 原样拷贝原 obj(仅修正 mtllib/usemtl 指向本目录的 youshui.mtl),
把新 mesh 追加在文件末尾(o Transparent / usemtl Transparent)。
只依赖 numpy; 所有读写都限制在本脚本所在目录。
与参考结果 youshui_result.obj 的对比: 结构一致(原模型 + o Transparent 水密闭合
包装 mesh); 本脚本只包装镜框正面核心(含镜片镂空填充), 不包裹向后弯折的镜腿,
因此与参考一样不超出眼镜正面外框、z 向深度一致; 参考在镜腿后另有一小片
手工处理的收拢薄膜, 此处不复现。
用法:
python3 vacuum_wrap.py
python3 vacuum_wrap.py --spacing 0.06 --thickness 3.4 --cell 0.05 --front 0.25
"""
import os
import glob
import argparse
import numpy as np
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
def resolve(p):
"""路径一律相对本脚本目录解析, 不碰其它目录。"""
return p if os.path.isabs(p) else os.path.join(SCRIPT_DIR, p)
# ----------------------------------------------------------------- OBJ 读取
def load_obj(path):
"""读取顶点与三角面(多于三个顶点的面做三角扇拆分)。"""
verts, faces = [], []
with open(path, 'r', errors='ignore') as f:
for line in f:
if line.startswith('v '):
p = line.split()
verts.append((float(p[1]), float(p[2]), float(p[3])))
elif line.startswith('f '):
ids = [int(x.split('/')[0]) - 1 for x in line.split()[1:]]
for k in range(1, len(ids) - 1):
faces.append((ids[0], ids[k], ids[k + 1]))
return np.asarray(verts, np.float64), np.asarray(faces, np.int64)
def load_index(path, nverts):
idx = []
with open(path) as f:
for line in f:
line = line.strip()
if line:
idx.append(int(line))
idx = np.asarray(idx, np.int64)
if len(idx) == 0:
raise RuntimeError('index.txt 为空')
if idx.min() < 0 or idx.max() >= nverts:
raise RuntimeError('index.txt 中索引越界(0..%d)' % (nverts - 1))
return idx
# ------------------------------------------------------------ 正面朝向判定
def pick_front_axis(verts, sub, wind, coarse):
"""判定“抽真空”的视线轴: 沿该轴投影时, 眼镜区域内部会出现封闭的
镜片镂空(孔洞), 孔洞面积最大的轴即包装轴(如从正面看眼镜)。
wind: 整mesh体积符号(+1=法线朝外), 用于确定正面在轴的正/负侧。
返回 (k, sgn, u轴, v轴), (u, v, w=sgn*第k轴) 构成右手系。"""
p0, p1, p2 = verts[sub[:, 0]], verts[sub[:, 1]], verts[sub[:, 2]]
n = np.cross(p1 - p0, p2 - p0)
sgn_area = wind * n.sum(axis=0) # 各轴带符号投影面积
best_hole, best_k = -1.0, 2
rc = max(2, int(round(0.6 / coarse))) # 较大闭运算: 桥接处覆盖稀疏时也能封住镂空
for k in range(3):
u, v = (k + 1) % 3, (k + 2) % 3
tri_uv = verts[sub][:, :, [u, v]]
tri_w = verts[sub][:, :, k]
cover, _, _ = rasterize(tri_uv, tri_w, coarse)
foot = fill_enclosed(binary_closing(cover, rc))
hole = float((foot & ~cover).sum()) * coarse * coarse
print(' 投影轴 %s: 表面覆盖 %.1f, 封闭镂空 %.1f'
% ('xyz'[k], cover.sum() * coarse * coarse, hole))
if hole > best_hole:
best_hole, best_k = hole, k
k = best_k
sgn = 1.0 if sgn_area[k] >= 0 else -1.0
return k, sgn, (k + 1) % 3, (k + 2) % 3
# ------------------------------------------------------------------ 栅格化
def rasterize(tri_uv, tri_w, h, keep=None, collect=False):
"""把子网格三角形投影到 (u,v) 平面栅格化。
keep: 可选的三角形布尔掩码(如只保留朝前的面)。
collect: 额外收集 每格覆盖的三角形编号表(用于后续精确求高)。
返回 cover(是否被覆盖) 与 topz(该格内最靠前的表面高度 w);
collect=True 时再附带 cell_tris。"""
umin, vmin = tri_uv[..., 0].min(), tri_uv[..., 1].min()
umax, vmax = tri_uv[..., 0].max(), tri_uv[..., 1].max()
# 留出边距(>= 闭运算半径+2), 保证膨胀/洪泛不会越过网格边界
mg = 8
umin -= mg * h; vmin -= mg * h
nu = int(np.ceil((umax - umin) / h)) + 1 + mg
nv = int(np.ceil((vmax - vmin) / h)) + 1 + mg
if keep is not None: # 网格 bbox 一律取自全集, 保证可对齐
tri_uv = tri_uv[keep]
tri_w = tri_w[keep]
nu -= nu % 2; nv -= nv % 2 # 偶数尺寸, 红黑迭代对齐
cover = np.zeros((nu, nv), bool)
topz = np.full((nu, nv), np.nan)
cell_tris = {} if collect else None
ntri = tri_uv.shape[0]
for t in range(ntri):
a = tri_uv[t, 0]; b = tri_uv[t, 1]; c = tri_uv[t, 2]
wa, wb, wc = tri_w[t]
i0 = max(0, int(np.floor((min(a[0], b[0], c[0]) - umin) / h)) - 1)
i1 = min(nu - 1, int(np.floor((max(a[0], b[0], c[0]) - umin) / h)) + 1)
j0 = max(0, int(np.floor((min(a[1], b[1], c[1]) - vmin) / h)) - 1)
j1 = min(nv - 1, int(np.floor((max(a[1], b[1], c[1]) - vmin) / h)) + 1)
if i1 < i0 or j1 < j0:
continue
cx = umin + (np.arange(i0, i1 + 1) + 0.5) * h
cy = vmin + (np.arange(j0, j1 + 1) + 0.5) * h
CX, CY = np.meshgrid(cx, cy, indexing='ij')
v0 = c - a
v1 = b - a
d00 = v0 @ v0; d01 = v0 @ v1; d11 = v1 @ v1
den = d00 * d11 - d01 * d01
if abs(den) < 1e-14: # 投影退化(近似垂直面), 跳过
continue
px, py = CX - a[0], CY - a[1]
d20 = px * v0[0] + py * v0[1]
d21 = px * v1[0] + py * v1[1]
bu = (d11 * d20 - d01 * d21) / den
bv = (d00 * d21 - d01 * d20) / den
ins = (bu >= -1e-9) & (bv >= -1e-9) & (bu + bv <= 1 + 1e-9)
if not ins.any():
continue
z = wa + bu * (wb - wa) + bv * (wc - wa)
sub = topz[i0:i1 + 1, j0:j1 + 1]
topz[i0:i1 + 1, j0:j1 + 1] = np.where(ins, np.fmax(sub, z), sub)
cover[i0:i1 + 1, j0:j1 + 1] |= ins
if collect:
ii, jj = np.nonzero(ins)
for ci_, cj_ in zip(ii, jj):
cell_tris.setdefault((int(ci_) + i0, int(cj_) + j0), []).append(t)
if collect:
return cover, topz, (umin, vmin), cell_tris
return cover, topz, (umin, vmin)
# ------------------------------------------------------- 精确薄膜高度
def exact_film_heights(pts, tri_uv, tri_w, cell_tris, u0, v0, h):
"""每个薄膜网格点沿视线轴与子网格求精确交点, 取最靠前的高度。
相比栅格最近格值, 没有分段常数的阶梯量化, 薄膜贴合更光滑。"""
n = len(pts)
Z = np.full(n, np.nan)
for p in range(n):
x, y = pts[p, 0], pts[p, 1]
i = int((x - u0) / h)
j = int((y - v0) / h)
ids = cell_tris.get((i, j))
if not ids:
continue
best = -np.inf
for t in ids:
a = tri_uv[t, 0]; b = tri_uv[t, 1]; c = tri_uv[t, 2]
e0 = c - a
e1 = b - a
d00 = e0 @ e0; d01 = e0 @ e1; d11 = e1 @ e1
den = d00 * d11 - d01 * d01
if abs(den) < 1e-14:
continue
d20 = (x - a[0]) * e0[0] + (y - a[1]) * e0[1]
d21 = (x - a[0]) * e1[0] + (y - a[1]) * e1[1]
bu = (d11 * d20 - d01 * d21) / den
bv = (d00 * d21 - d01 * d20) / den
if bu >= -1e-9 and bv >= -1e-9 and bu + bv <= 1 + 1e-9:
wa, wb, wc = tri_w[t]
z = wa + bu * (wb - wa) + bv * (wc - wa)
if z > best:
best = z
if best > -np.inf:
Z[p] = best
return Z
# ---------------------------------------------------------------- 形态学
def dilate(m):
r = m.copy()
r[1:, :] |= m[:-1, :]
r[:-1, :] |= m[1:, :]
r[:, 1:] |= m[:, :-1]
r[:, :-1] |= m[:, 1:]
return r
def erode(m):
r = m.copy()
r[1:, :] &= m[:-1, :]
r[:-1, :] &= m[1:, :]
r[:, 1:] &= m[:, :-1]
r[:, :-1] &= m[:, 1:]
return r
def binary_closing(mask, r):
if r <= 0:
return mask.copy()
m = np.pad(mask, r, constant_values=False) # pad 足够大, 膨胀不会越界
for _ in range(r):
m = dilate(m)
for _ in range(r):
m = erode(m)
return m[r:-r, r:-r]
def fill_enclosed(mask):
"""从外部洪泛, 补上内部封闭的镂空(镜片孔洞), 开向边界的凹口(鼻梁)保留。"""
outside = np.zeros_like(mask)
outside[0, :] = ~mask[0, :]
outside[-1, :] = ~mask[-1, :]
outside[:, 0] = ~mask[:, 0]
outside[:, -1] = ~mask[:, -1]
while True:
new = dilate(outside) & ~mask
if np.array_equal(new, outside):
break
outside = new
return ~outside
def largest_filled_component(mask):
"""先补内部封闭孔洞, 再取面积最大的连通组件(含镜片镂空的镜框核心),
丢掉镜腿等散碎覆盖 → 包装不会越过镜框正面外框。"""
filled = fill_enclosed(mask)
BIG = filled.size + 1
lab = np.where(filled, np.arange(filled.size).reshape(filled.shape), BIG)
while True: # 4邻域最小标签传播 → 连通组件标记
new = lab.copy()
for (s1, s2) in (((slice(1, None), slice(None)), (slice(None, -1), slice(None))),
((slice(None, -1), slice(None)), (slice(1, None), slice(None))),
((slice(None), slice(1, None)), (slice(None), slice(None, -1))),
((slice(None), slice(None, -1)), (slice(None), slice(1, None)))):
cand = np.full_like(lab, BIG)
cand[s1] = lab[s2]
cand[~filled] = BIG # 标签只在组件内传播, 不漏进空格
np.minimum(new, cand, out=new)
if np.array_equal(new, lab):
break
lab = new
flat = lab.ravel()
labels = flat[flat < BIG]
if len(labels) == 0:
raise RuntimeError('没有可用连通组件')
best_lab, best_area = None, -1
for L in np.unique(labels):
a = int((flat == L).sum())
if a > best_area:
best_area, best_lab = a, L
return lab == best_lab
# ----------------------------------------------------------- 外轮廓追踪
def trace_all_cycles(mask):
"""沿 cell 边缘追踪 mask 的所有边界环(内部在行进方向左侧)。"""
nu, nv = mask.shape
edges = {}
def add(a, b):
edges.setdefault(a, []).append(b)
I, J = np.nonzero(mask)
for i, j in zip(I, J):
if i == 0 or not mask[i - 1, j]: # 左侧外部 → 向 -v 走
add((i, j + 1), (i, j))
if i == nu - 1 or not mask[i + 1, j]: # 右侧外部 → 向 +v 走
add((i + 1, j), (i + 1, j + 1))
if j == 0 or not mask[i, j - 1]: # 下侧外部 → 向 +u 走
add((i, j), (i + 1, j))
if j == nv - 1 or not mask[i, j + 1]: # 上侧外部 → 向 -u 走
add((i + 1, j + 1), (i, j + 1))
cycles, visited = [], set()
for start in edges:
if start in visited:
continue
cyc = [start]; visited.add(start); cur = start
while True:
nxts = [e for e in edges.get(cur, ()) if e not in visited]
if not nxts:
break
nxt = nxts[0]
cyc.append(nxt); visited.add(nxt); cur = nxt
cycles.append(cyc)
return cycles
def signed_area(cyc):
a = 0.0
for k in range(len(cyc)):
x1, y1 = cyc[k]; x2, y2 = cyc[(k + 1) % len(cyc)]
a += x1 * y2 - x2 * y1
return a / 2.0
def trace_outer_cycle(mask):
"""返回 (最大面积的边界环, 显著环个数)。
环的方向统一为逆时针(内部在左侧)。"""
cycles = trace_all_cycles(mask)
if not cycles:
raise RuntimeError('外轮廓追踪失败: mask 无边界')
cycles.sort(key=lambda c: abs(signed_area(c)), reverse=True)
main = cycles[0]
if signed_area(main) < 0:
main = main[::-1]
ref = abs(signed_area(main))
n_sig = 1 + sum(1 for c in cycles[1:]
if abs(signed_area(c)) > 0.02 * ref)
return main, n_sig
# -------------------------------------------------------------- 点在多边形内
def points_in_poly(pts, poly):
"""even-odd 规则, pts:(N,2), poly:(K,2) 隐式闭合。"""
x, y = pts[:, 0], pts[:, 1]
inside = np.zeros(len(pts), bool)
x1, y1 = poly[:, 0], poly[:, 1]
x2, y2 = np.roll(x1, -1), np.roll(y1, -1)
with np.errstate(divide='ignore', invalid='ignore'):
for k in range(len(poly)):
cond = (y1[k] > y) != (y2[k] > y)
xint = (x2[k] - x1[k]) * (y - y1[k]) / (y2[k] - y1[k]) + x1[k]
inside ^= cond & (x < xint)
return inside
# --------------------------------------------- 镂空处高度场(与镜框平齐)
def _neighbor_avg4(Z, ok_mask):
"""4 邻域均值: 只统计 ok_mask 为真且值非 nan 的格子。
返回 (均值, 计数)。"""
acc = np.zeros(Z.shape)
cnt = np.zeros(Z.shape)
nanZ = np.isnan(Z)
for sl_src, sl_dst in ((slice(None, -1), slice(1, None)),
(slice(1, None), slice(None, -1))):
have = ok_mask[sl_src, :] & ~nanZ[sl_src, :]
acc[sl_dst, :] += np.where(have, Z[sl_src, :], 0.0)
cnt[sl_dst, :] += have
have = ok_mask[:, sl_src] & ~nanZ[:, sl_src]
acc[:, sl_dst] += np.where(have, Z[:, sl_src], 0.0)
cnt[:, sl_dst] += have
return np.where(cnt > 0, acc / np.maximum(cnt, 1), np.nan), cnt
def _fit_surface(di, dj, z):
"""最小二乘拟合 z ≈ a + b·di + c·dj (平面趋势)。
采样点只分布在镂空周围的环带上: 二次趋势面对这种采样是病态
外推(环带上常数项与二次项近似共线, 中心值可飘出 ±2), 平面
趋势则稳定; 曲率起伏留给残差调和项(边界值插值, 良定)。
点太少/退化时回退到常数。返回 (系数, 项数)。"""
n = len(z)
di = di.astype(np.float64)
dj = dj.astype(np.float64)
if n >= 7:
A = np.stack([np.ones(n), di, dj], 1)
G = A.T @ A
G[np.diag_indices_from(G)] += 1e-6 * max(1.0, G.diagonal().max())
try:
return np.linalg.solve(G, A.T @ z), 3
except np.linalg.LinAlgError:
pass
return np.array([float(np.mean(z))]), 1
def _eval_surface(coef, nterm, di, dj):
di = np.asarray(di, np.float64)
dj = np.asarray(dj, np.float64)
if nterm == 3:
return coef[0] + coef[1] * di + coef[2] * dj
return np.full(di.shape, coef[0], float)
def trend_fill(Z, valid, select=None, blend_passes=500, omega=0.8):
"""镜片镂空处高度场填充: 全局平面趋势 + 残差调和, 平齐且平滑。
薄膜要"平齐"(跟随镜框正面的倾斜)也要平滑(无台阶无尖峰):
1) 用正面表面(select)上的贴合点拟合平面趋势 T(镜框正面倾角)。
采样点铺满整片 footprint, 平面拟合稳定(二次面在只环绕
镜片镂空的环带采样下是病态外推, 中心值可飘出 ±2);
2) 残差 R = 贴合高度 - T 在**所有**贴合点锚定(含外缘陡边/
镜托/镜腿根处的深锚点——薄膜要包住它们), 镂空处解
Laplace 方程: 调和解无内部极值、不会过冲, 天然平滑;
镜片中央 ≈ 趋势面(平齐), 孔缘精确衔接, 深锚点周围形成
光滑下凹而不是尖 crater。
blend_passes 为残差调和迭代次数; 迭代用欠松弛(ω≤1)——
超松弛会放大棋盘模式留下二阶差分尖峰。"""
Z = Z.copy()
unknown = valid & np.isnan(Z)
if not unknown.any():
return Z
known = valid & ~np.isnan(Z)
# 1) 平面趋势: 只用正面表面高度, 稳健剔除离群锚点
K = known if select is None else (known & select)
if K.sum() >= 7:
ki, kj = np.nonzero(K)
for _ in range(2):
coef, nterm = _fit_surface(ki - ki.mean(), kj - kj.mean(),
Z[ki, kj])
if len(ki) < 16:
break
res = Z[ki, kj] - _eval_surface(
coef, nterm, ki - ki.mean(), kj - kj.mean())
keep = np.abs(res) <= max(0.12, 4.0 * np.median(np.abs(res)))
if keep.all() or keep.sum() < 8:
break
ki, kj = ki[keep], kj[keep]
ci, cj = ki.mean(), kj.mean()
elif K.sum() > 0:
coef, nterm = np.array([float(np.mean(Z[K]))]), 1
ci = cj = 0.0
else:
coef, nterm = np.array([float(np.nanmean(Z[known]))]), 1
ci = cj = 0.0
ii, jj = np.indices(Z.shape)
T = _eval_surface(coef, nterm, ii - ci, jj - cj)
# 2) 残差调和填充: 全体镂空处一起解, 锚定所有贴合点
R = np.where(known, Z - T, 0.0)
for _ in range(blend_passes):
avg, cnt = _neighbor_avg4(R, valid)
upd = unknown & (cnt > 0)
if not upd.any():
break
R[upd] += omega * (avg[upd] - R[upd])
Z[unknown] = T[unknown] + R[unknown]
return Z
# ------------------------------------------------------------ 贴合带平滑
def smooth_adherence(Z, known, valid, thr, spacing,
alpha=0.6, passes=60, near=0.3, near_w=0.4):
"""贴合带受限平滑(消除镜框陡坡处高度场的棱面)。
高度场 z(u,v) 表示坡度为 s 的表面时, 其曲率会被放大 ~s^2 倍,
镜框侧弯坡度大处逐格贴合会出现 30-60 度的相邻面法线跳变。
本函数把与 8 邻域均值偏差超过 thr 的点向邻域均值加权松弛:
- 贴合点(known) 权重 1: 磨掉曲率突变棱面, 薄膜在陡坡处"绷而不贴";
坡度平缓处/均匀斜面的偏差本来就小于 thr, 不会被改动;
- 绷紧区离贴合边界 near 以内的点权重 near_w: 轻推, 软化接缝褶皱;
- 更远处不参与(保持真空 Laplace 解)。
thr 的量纲是高度: 相邻面法线跳变约 atan(4*thr/spacing)。"""
band = known.copy()
for _ in range(max(1, int(round(near / spacing)))):
band = dilate(band)
w = np.where(known, 1.0, np.where(band & valid, near_w, 0.0))
Z = Z.copy()
ni, nj = Z.shape
for _ in range(passes):
Zp = np.pad(Z, 1, constant_values=np.nan)
Vp = np.pad(valid, 1, constant_values=False)
acc = np.zeros(Z.shape)
cnt = np.zeros(Z.shape)
for di in (-1, 0, 1):
for dj in (-1, 0, 1):
if di == 0 and dj == 0:
continue
sl = (slice(1 + di, 1 + di + ni), slice(1 + dj, 1 + dj + nj))
nb = np.where(Vp[sl], Zp[sl], np.nan)
ok = ~np.isnan(nb)
acc += np.where(ok, nb, 0.0)
cnt += ok
avg = np.where(cnt > 0, acc / np.maximum(cnt, 1), np.nan)
res = np.abs(Z - avg)
upd = (w > 0) & (cnt > 0) & (res > thr)
if not upd.any():
break
Z[upd] += w[upd] * alpha * (avg[upd] - Z[upd])
return Z, w
# ------------------------------------------------------------ 网格清理
def drop_small_components(faces, tags=None):
"""只保留面数最多的连通组件(正常情况下包装 mesh 只有唯一一层壳,
栅格噪声/异常轮廓可能产生碎块或多壳, 一律丢弃)。
tags: 面标签(0=顶面 1=底面 2=侧壁), 随面一同过滤; 返回 faces
或 (faces, tags)。"""
from collections import defaultdict
parent = list(range(len(faces)))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
e2f = defaultdict(list)
for fi, f in enumerate(faces):
for x, y in ((f[0], f[1]), (f[1], f[2]), (f[2], f[0])):
e2f[(min(x, y), max(x, y))].append(fi)
for fl in e2f.values():
for fi in fl[1:]:
parent[find(fi)] = find(fl[0])
groups = defaultdict(int)
for fi in range(len(faces)):
groups[find(fi)] += 1
if len(groups) <= 1:
return (faces, tags) if tags is not None else faces
best = max(groups.values())
if best < len(faces) * 0.9:
print(' 警告: 网格有 %d 个组件, 仅保留最大者(%d/%d 面)'
% (len(groups), best, len(faces)))
keep = [fi for fi in range(len(faces)) if groups[find(fi)] == best]
if len(keep) != len(faces):
keep = np.asarray(keep)
faces = faces[keep]
if tags is not None:
tags = tags[keep]
return (faces, tags) if tags is not None else faces
def split_pinch_vertices(verts, faces):
"""拆分“夹点”顶点(一个顶点被多片不相连的面共享, 网格对角接触造成),
拆分后网格为严格的顶点流形。返回 (verts, faces)。"""
from collections import defaultdict
vfaces = defaultdict(list)
for fi, f in enumerate(faces):
for v in f:
vfaces[int(v)].append(fi)
# v -> [ (本组面id列表), ... ]
splits = []
for v, fl in vfaces.items():
if len(fl) < 2:
continue
edge_faces = defaultdict(list)
for fi in fl:
f = faces[fi]
for x, y in ((f[0], f[1]), (f[1], f[2]), (f[2], f[0])):
u = y if x == v else (x if y == v else None)
if u is not None:
edge_faces[int(u)].append(fi)
parent = {fi: fi for fi in fl}
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
for fl2 in edge_faces.values():
for fi in fl2[1:]:
parent[find(fi)] = find(fl2[0])
groups = defaultdict(list)
for fi in fl:
groups[find(fi)].append(fi)
if len(groups) > 1:
splits.append((v, [g for g in groups.values()]))
if not splits:
return verts, faces
faces = faces.copy()
extra_verts = []
for v, groups in splits:
for g in groups[1:]:
nv = len(verts) + len(extra_verts)
extra_verts.append(verts[v])
for fi in g:
f = faces[fi]
faces[fi] = [nv if x == v else x for x in f]
if extra_verts:
verts = np.vstack([verts, np.asarray(extra_verts)])
return verts, faces
# ------------------------------------------------------------ 网格松弛
def smooth_closed_curve(P, passes=3, lam=0.5, mu=-0.53):
"""闭合折线 Taubin(λ/μ) 低通平滑: 消除栅格楼梯而不收缩轮廓。
交替"向邻点均值收缩(λ)"与"反向外推(μ)", 大尺度形状(鼻梁凹口、
镜框圆角)保留, 只磨掉 cell 量级的锯齿。用于外轮廓多边形。"""
P = np.array(P, np.float64, copy=True)
for _ in range(passes):
for a in (lam, mu):
avg = 0.5 * (np.roll(P, 1, axis=0) + np.roll(P, -1, axis=0))
P = P + a * (avg - P)
return P
def smooth_rim_curve(verts, faces, tags, passes=20, lam=0.5):
"""薄膜周边收边: 沿顶膜边界曲线做 1D 拉普拉斯平滑。
footprint 是网格点掩码, 薄膜边缘天生带 spacing 级楼梯。本函数:
- 取顶面(tags==0)的边界边 → 每个边缘顶点恰有两个曲线邻居,
沿这条闭合曲线反复做拉普拉斯松弛, 把楼梯磨成圆角;
曲线点数近千、点距 ~spacing, 整体收缩量可忽略;
- 底膜边缘顶点(顶/底膜是同一网格点的上下两个副本, 组装时
(u,v) 相同)通过侧壁找到配对, 加上与顶膜边缘相同的位移,
侧壁整体随缘平移, 不会被剪切;
- 薄膜内部完全不被动(贴合不受影响)。
注意: 不能改用对整个闭合 mesh 的拉普拉斯松弛来做收边/平滑——
侧壁是 spacing x thickness 的细长三角形(长宽比可达 70:1),
伞形算子会把膜面边缘顶点拽向侧壁另一端的底膜顶点, 反而在
周边产生大幅 z 向锯齿(膜面顶点也会被拉伸错位)。
tags: build_wrap_mesh 的面标签(0=顶面 1=底面 2=侧壁)。"""
from collections import Counter, defaultdict
topF = faces[tags == 0]
botF = faces[tags == 1]
wallF = faces[tags == 2]
in_bot = np.zeros(len(verts), bool)
in_bot[botF.ravel()] = True
# 顶膜边界无向边(在顶面内出现一次的边) → 曲线邻接表
cnt = Counter()
for f in topF:
for x, y in ((f[0], f[1]), (f[1], f[2]), (f[2], f[0])):
cnt[(min(x, y), max(x, y))] += 1
nbr = {}
for (a, b) in (e for e, c in cnt.items() if c == 1):
nbr.setdefault(int(a), []).append(int(b))
nbr.setdefault(int(b), []).append(int(a))
# 顶/底膜边缘配对: 与顶膜边缘顶点共享侧壁且 (u,v) 相同的底膜点
v2wall = defaultdict(list)
for fi, f in enumerate(wallF):
for x in f:
v2wall[int(x)].append(fi)
keys = sorted(nbr.keys())
pair = {}
for v in keys:
pv = verts[v]
for fi in v2wall.get(v, ()):
for x in wallF[fi]:
x = int(x)
if in_bot[x] and abs(verts[x, 0] - pv[0]) < 1e-9 \
and abs(verts[x, 1] - pv[1]) < 1e-9:
pair[v] = x
# 沿曲线拉普拉斯松弛(Jacobi)
pos = {v: verts[v].copy() for v in keys}
for _ in range(passes):
new = {}
for v, P in pos.items():
a, b = nbr[v]
new[v] = P + lam * (0.5 * (pos[a] + pos[b]) - P)
pos = new
# 应用: 顶膜边缘取新位置, 底膜配对点加相同位移(侧壁随缘平移)
for v in keys:
delta = pos[v] - verts[v]
verts[v] = pos[v]
b = pair.get(v)
if b is not None:
verts[b] += delta
return verts
# ------------------------------------------------------------ 包装 mesh 组装
def build_wrap_mesh(U, V, Z, inside, thickness, w_floor=None):
"""顶面膜 + 底面膜 + 侧壁 → 闭合网格。
顶点/面均为 (u,v,w) 坐标, 返回 (verts, faces, tags),
tags: 每个面的标签(0=顶面 1=底面 2=侧壁), 供收边平滑
定位薄膜边缘(周边锯齿所在的侧壁)。
w_floor: 底面膜最深不超过该值(默认=镜架最后缘再退 0.1),
防止薄膜沿侧影线下潜时背面膜探出镜架范围。"""
ni, nj = Z.shape
idx = -np.ones((ni, nj), np.int64)
ii, jj = np.nonzero(inside)
n = len(ii)
idx[ii, jj] = np.arange(n)
zt = Z[ii, jj]
zb = zt - thickness
if w_floor is not None:
zb = np.maximum(zb, w_floor)
verts = np.concatenate([
np.stack([U[ii, jj], V[ii, jj], zt], axis=1),
np.stack([U[ii, jj], V[ii, jj], zb], axis=1),
])
# 单元格四角: a=(i,j) b=(i+1,j) c=(i+1,j+1) d=(i,j+1)
a = idx[:-1, :-1]; b = idx[1:, :-1]; c = idx[1:, 1:]; d = idx[:-1, 1:]
ok = (a >= 0) & (b >= 0) & (c >= 0) & (d >= 0)
ci, cj = np.nonzero(ok)
A, B, C, D = a[ci, cj], b[ci, cj], c[ci, cj], d[ci, cj]
# 棋盘式交替对角线: 相邻单元的剖分方向相反, 避免整片同向的机械条纹
m0 = ((ci + cj) % 2 == 0)
topF = np.concatenate([
np.stack([A[m0], B[m0], C[m0]], 1), np.stack([A[m0], C[m0], D[m0]], 1),
np.stack([A[~m0], B[~m0], D[~m0]], 1), np.stack([B[~m0], C[~m0], D[~m0]], 1)])
botF = np.concatenate([
np.stack([A[m0], C[m0], B[m0]], 1), np.stack([A[m0], D[m0], C[m0]], 1),
np.stack([A[~m0], D[~m0], B[~m0]], 1), np.stack([B[~m0], D[~m0], C[~m0]], 1)]) + n
# 顶面的边界有向边(内部在左侧) → 缝合到背面
# 先做无向计数找边界(内部边在相邻两面中方向相反、各出现一次),
# 再取其在面中出现的方向用于侧壁绕向
from collections import Counter
und = Counter()
directed = []
for f in topF:
for x, y in ((f[0], f[1]), (f[1], f[2]), (f[2], f[0])):
key = (x, y) if x < y else (y, x)
und[key] += 1
directed.append((x, y))
bndset = {e for e, c in und.items() if c == 1}
rim_pairs = [(p, q) for (p, q) in directed
if ((p, q) if p < q else (q, p)) in bndset]
rim = [[q, p, p + n] for (p, q) in rim_pairs]
rim += [[q, p + n, q + n] for (p, q) in rim_pairs]
faces = np.concatenate([topF, botF, np.asarray(rim, np.int64)])
tags = np.concatenate([np.zeros(len(topF), np.int8),
np.ones(len(botF), np.int8),
np.full(len(rim), 2, np.int8)])
# 去掉未被任何面引用的孤立顶点
faces, tags = drop_small_components(faces, tags)
verts, faces = split_pinch_vertices(verts, faces)
used = np.zeros(len(verts), bool)
used[faces.ravel()] = True
remap = -np.ones(len(verts), np.int64)
remap[used] = np.arange(used.sum())
return verts[used], remap[faces], tags
def check_closed(faces, nverts):
"""水密性与拓扑自检: 每条有向边恰好出现一次且其反向也存在
(方向一致的水密闭合), 欧拉示性数 V - E + F = 2。"""
d = {}
for f in faces:
for x, y in ((f[0], f[1]), (f[1], f[2]), (f[2], f[0])):
d[(x, y)] = d.get((x, y), 0) + 1
bad = [e for e, c in d.items() if c != 1 or (e[1], e[0]) not in d]
n_e = 3 * len(faces) // 2
chi = nverts - n_e + len(faces)
return len(bad) == 0, chi
# ------------------------------------------------------------------- 主流程
def default_obj_path():
"""自动探测输入模型: 取脚本所在目录下最大的 *.obj,
排除历史输出(*_result/wrap_result/*_fill* 等)。"""
cands = []
for p in glob.glob(os.path.join(SCRIPT_DIR, '*.obj')):
b = os.path.basename(p).lower()
if 'result' in b or 'wrap' in b or '_fill' in b:
continue
cands.append((os.path.getsize(p), p))
if not cands:
return None
return max(cands)[1]
def main():
ap = argparse.ArgumentParser(description='眼镜“抽真空包装”mesh 生成')
ap.add_argument('--obj', default=None,
help='原始模型 obj(默认: 自动探测本目录最大的非输出 obj)')
ap.add_argument('--index', default=None,
help='眼镜顶点索引(默认: 模型同目录的 index.txt)')
ap.add_argument('--out', default=None,
help='输出路径(默认: 模型同目录 <模型名>_wrap_result.obj)')
ap.add_argument('--cell', type=float, default=0.05,
help='外轮廓栅格分辨率(模型单位, 默认0.05)')
ap.add_argument('--spacing', type=float, default=0.06,
help='包装薄膜网格点间距(默认0.06)')
ap.add_argument('--thickness', type=float, default=3.4,
help='背面膜相对薄膜的后退厚度(默认3.4)')
ap.add_argument('--front', type=float, default=0.25,
help='朝前面阈值(法线与视线轴余弦, 默认0.25); '
'调小会包含更斜的面乃至镜腿, 调大只贴更正对的面')
ap.add_argument('--closing', type=int, default=4,
help='形态学闭运算半径(格数, 默认4)')
ap.add_argument('--hole-blend', type=int, default=500,
help='镜片镂空处残差调和的迭代次数(默认500): '
'趋势面跟随镜框平齐, 欠松弛迭代负责孔缘平滑衔接; '
'越大越平滑')
ap.add_argument('--rim-smooth', type=int, default=20,
help='周边收边平滑次数(0=关闭, 默认20): 沿薄膜边缘曲线 '
'拉普拉斯平滑, 磨掉轮廓栅格锯齿')
ap.add_argument('--poly-smooth', type=int, default=3,
help='外轮廓多边形 Taubin 平滑次数(0=关闭, 默认3), '
'从源头去掉 cell 级楼梯')
ap.add_argument('--band-smooth', type=float, default=0.035,
help='贴合带平滑阈值(×spacing, 0=关闭; 磨掉陡坡棱面)')
ap.add_argument('--mtl', default=None,
help='覆盖 mtllib 行的材料文件名(默认自动找本目录 *.mtl)')
args = ap.parse_args()
if args.obj is None:
args.obj = default_obj_path()
if args.obj is None:
raise RuntimeError('未指定 --obj, 且本目录没有可自动探测的模型文件')
obj_path = resolve(args.obj)
obj_dir = os.path.dirname(obj_path)
if args.index is None:
args.index = os.path.join(obj_dir, 'index.txt')
if args.out is None:
stem = os.path.splitext(os.path.basename(obj_path))[0]
args.out = os.path.join(obj_dir, stem + '_wrap_result.obj')
idx_path, out_path = resolve(args.index), resolve(args.out)
print('[1/6] 读取 %s ...' % os.path.basename(obj_path))
verts, faces = load_obj(obj_path)
print(' %d 顶点, %d 三角面' % (len(verts), len(faces)))
gidx = load_index(idx_path, len(verts))
print(' index.txt: %d 个眼镜顶点' % len(gidx))
# ---- 眼镜子网格 & 正面轴向
in_g = np.zeros(len(verts), bool)
in_g[gidx] = True
sel = in_g[faces].all(axis=1)
sub = faces[sel]
if len(sub) < 10:
raise RuntimeError('眼镜子网格面太少(%d), 请检查 index.txt' % len(sub))
print(' 眼镜子网格: %d 面' % len(sub))
# 整 mesh 体积符号 → 面法线是否朝外
p0, p1, p2 = verts[faces[:, 0]], verts[faces[:, 1]], verts[faces[:, 2]]
vol = np.einsum('ij,ij->i', p0, np.cross(p1, p2)).sum() / 6.0
wind = 1.0 if vol >= 0 else -1.0
coarse = max(0.15, args.cell * 1.5)
print(' 判定包装视线轴(封闭镂空最大者):')
k, sgn, ax_u, ax_v = pick_front_axis(verts, sub, wind, coarse)
print(' 正面视线轴 = %s, u=轴%d(%s), v=轴%d(%s)'
% ('xyz'[k] + ('+' if sgn > 0 else '-'),
ax_u, 'xyz'[ax_u], ax_v, 'xyz'[ax_v]))
tri_uv = verts[sub][:, :, [ax_u, ax_v]]
tri_w = sgn * verts[sub][:, :, k]
# ---- 朝前面掩码(法线与视线轴余弦) → 只包装镜框正面, 排除向后弯折的镜腿
sp0, sp1, sp2 = verts[sub[:, 0]], verts[sub[:, 1]], verts[sub[:, 2]]
nrm = np.cross(sp1 - sp0, sp2 - sp0)
ln = np.linalg.norm(nrm, axis=1)
front = (sgn * nrm[:, k] / np.where(ln > 1e-12, ln, 1.0)) > args.front
print(' 朝前面: %d/%d (front>%.2f)' % (front.sum(), len(sub), args.front))
# ---- 栅格化 → 外轮廓(闭运算补缝 → 只取含镜片镂空的镜框核心组件,
# 内部封闭孔洞填满; 半径自适应有限加大, 不会封死鼻梁凹口)
print('[2/6] 栅格化朝前表面 (cell=%.2f) ...' % args.cell)
cover, topz, (u0, v0) = rasterize(tri_uv, tri_w, args.cell, front)
cover_all, topz_all, (ua, va), cell_tris = rasterize(
tri_uv, tri_w, args.cell, collect=True)
assert (ua, va) == (u0, v0) # 两次栅格化共用同一网格
print(' 朝前表面覆盖 %d/%d 格' % (cover.sum(), cover.size))
r = args.closing
cap = max(args.closing + 4, int(round(1.0 / args.cell)))
cover_area = float(cover.sum()) * args.cell ** 2
while True:
# 镜片镂空改以"全表面轮缘环"的封闭孔判定:
# 轮缘正面覆盖稀疏/有缺口时, 正面环闭不上会漏填整块镜片
# (632587 右镜片曾缺失 1/3); 全表面环更完整, 而 开向边界的
# 鼻梁凹口不属于封闭孔, 不会被误填
ring = binary_closing(cover_all, r)
holes = fill_enclosed(ring) & ~ring
foot = largest_filled_component(binary_closing(cover, r) | holes)
# 上/下缘的掠射面不在正面覆盖内但属于镜架:
# 正面 footprint 附近 0.2 内的轮缘格也纳入, 包装才铺满镜框外缘
near = foot.copy()
for _ in range(4):
near = dilate(near)
foot = largest_filled_component(foot | (ring & near))
cycle, n_sig = trace_outer_cycle(foot)
cyc_area = abs(signed_area(cycle)) * args.cell ** 2
foot_area = float(foot.sum()) * args.cell ** 2
# 判据一: 外轮廓单环完整(追踪不破);
# 判据二: 镜片镂空被填上(footprint 达到朝前覆盖的 2 倍以上,
# 覆盖稀疏/有缺口的数据需要更大的闭运算半径)
if (cyc_area >= 0.9 * foot_area and foot_area >= 2.0 * cover_area) \
or r >= cap:
break
print(' 闭运算半径 %d → 外轮廓覆盖 footprint 的 %.0f%%, '
'镂空填充率 %.2f, 继续加大...'
% (r, 100 * cyc_area / foot_area, foot_area / cover_area))
r += 2
if foot_area < 1.5 * cover_area:
print(' 注意: 镜片镂空可能未被填满(填充率 %.2f), '
'可尝试加大 --closing' % (foot_area / cover_area))
print(' 镜框正面 footprint: %d 格(面积 %.1f), 闭运算半径 %d'
% (foot.sum(), foot_area, r))
poly = np.array([[u0 + i * args.cell, v0 + j * args.cell] for (i, j) in cycle],
dtype=np.float64)
print(' 外轮廓多边形: %d 个角点' % len(poly))
if args.poly_smooth > 0:
poly = smooth_closed_curve(poly, passes=args.poly_smooth)
print(' 外轮廓 Taubin 平滑 %d 次(消除 cell 级栅格楼梯, 不收缩轮廓)'
% args.poly_smooth)
# ---- 薄膜网格点
print('[3/6] 铺薄膜网格 (spacing=%.2f) ...' % args.spacing)
s = args.spacing
gu0 = poly[:, 0].min() - s; gu1 = poly[:, 0].max() + s
gv0 = poly[:, 1].min() - s; gv1 = poly[:, 1].max() + s
ni = int(np.ceil((gu1 - gu0) / s)) + 1
nj = int(np.ceil((gv1 - gv0) / s)) + 1
GU, GV = np.meshgrid(gu0 + np.arange(ni) * s,
gv0 + np.arange(nj) * s, indexing='ij')
pts = np.stack([GU.ravel(), GV.ravel()], axis=1)
ins = points_in_poly(pts, poly).reshape(ni, nj)
print(' 轮廓内网格点: %d' % ins.sum())
if ins.sum() < 4:
raise RuntimeError('轮廓内网格点太少, 请增大 --spacing 或减小 --cell')
# ---- 按镜架整体投影剪裁: 闭运算/轮缘带会把 footprint 向外胀出
# 0.1~0.2 的薄裙边(镜架上缘/下缘可见"超出"), 以镜架外轮廓
# (全表面投影 + 补采样缝 + 填充内部镂空)为准裁掉
frame_sil = fill_enclosed(binary_closing(cover_all, 2))
ic = np.clip(np.round((GU - u0) / args.cell).astype(int),
0, frame_sil.shape[0] - 1)
jc = np.clip(np.round((GV - v0) / args.cell).astype(int),
0, frame_sil.shape[1] - 1)
n_clip = int((ins & ~frame_sil[ic, jc]).sum())
if n_clip:
ins &= frame_sil[ic, jc]
print(' 按镜架投影裁掉外轮廓之外的裙边: %d 个网格点' % n_clip)
# 趋势拟合的锚点只取正面表面(cover 膨胀 3 格)上的贴合高度:
# 外缘陡边/掠射面处薄膜本该顺侧影下潜, 高度不能拿来外推镜片平面
cov_d = cover.copy()
for _ in range(3):
cov_d = dilate(cov_d)
front_sel = cov_d[ic, jc]
# ---- 顶面高度: 贴表面 + 镂空处与镜框平齐
print('[4/6] 计算薄膜高度(贴眼镜表面, 镜片处与镜框平齐) ...')
Z = exact_film_heights(pts, tri_uv, tri_w, cell_tris,
u0, v0, args.cell).reshape(ni, nj)
known = ~np.isnan(Z) & ins
n_hole = int((ins & ~known).sum())
print(' %d 点直接贴到表面, %d 点位于镂空处(趋势平齐填充)'
% (known.sum(), n_hole))
Z = trend_fill(Z, ins, select=front_sel, blend_passes=args.hole_blend)
# ---- 贴合带平滑: 镜框侧弯坡度大处, 高度场曲率被坡度放大成棱面
# (相邻面法线跳变 30-60 度), 受限松弛磨掉棱面并软化接缝;
# 浅坡处与均匀斜面本来就光滑, 不会被改动
if args.band_smooth > 0:
thr = args.band_smooth * args.spacing
Zs, w = smooth_adherence(Z, known, ins, thr, args.spacing)
n_up = int((np.abs(Zs - Z) > 1e-6).sum())
if n_up:
# 松弛后的点(含软化接缝带)作为新边界, 镂空处重新平齐外推
Z = trend_fill(np.where(w > 0, Zs, np.nan), ins,
select=front_sel, blend_passes=args.hole_blend)
print(' 贴合带平滑: %d 个点被松弛(阈值 %.4f), '
'镂空处重新平齐' % (n_up, thr))
# ---- 组装闭合 mesh
print('[5/6] 组装水密包装 mesh (thickness=%.2f) ...' % args.thickness)
# 底面膜深度钳制: 不超过镜架最后缘(留 0.1 薄膜厚度),
# 防止薄膜沿侧影线下潜时背面膜探出镜架范围
w_floor = float((sgn * verts[sub][:, :, k]).min()) - 0.1
wverts, wfaces, ftags = build_wrap_mesh(GU, GV, Z, ins, args.thickness, w_floor)
if args.rim_smooth > 0:
# 周边收边: 顶膜边缘沿边界曲线 1D 拉普拉斯, 磨掉栅格楼梯,
# 底膜边缘同步位移(侧壁随缘平移), 薄膜内部不动。
# 注意: 不做整 mesh 的拉普拉斯松弛——侧壁细长三角形会把
# 伞形算子变成"跨 3~4 个单位距离的强混合", 边缘反而出锯齿。
print(' 周边收边: 边缘曲线拉普拉斯平滑 %d 次 ...' % args.rim_smooth)
wverts = smooth_rim_curve(wverts, wfaces, ftags, passes=args.rim_smooth)
closed, chi = check_closed(wfaces, len(wverts))
print(' %d 顶点, %d 面; 水密=%s, 欧拉示性数=%d'
% (len(wverts), len(wfaces), closed, chi))
if not closed:
raise RuntimeError('生成的网格不是水密闭合网格, 请检查参数')
# 变换回世界坐标: (u, v, w) -> 原坐标
W = sgn * wverts[:, 2]
verts_world = np.zeros_like(wverts)
verts_world[:, ax_u] = wverts[:, 0]
verts_world[:, ax_v] = wverts[:, 1]
verts_world[:, k] = W
# 顶点法线(面积加权)
p0, p1, p2 = verts_world[wfaces[:, 0]], verts_world[wfaces[:, 1]], verts_world[wfaces[:, 2]]
fn = np.cross(p1 - p0, p2 - p0)
vn = np.zeros_like(verts_world)
for c in range(3):
np.add.at(vn, wfaces[:, c], fn)
ln = np.linalg.norm(vn, axis=1, keepdims=True)
vn = vn / np.where(ln > 1e-12, ln, 1.0)
# ---- 写出 obj
print('[6/6] 写出 %s ...' % os.path.basename(out_path))
mtl = args.mtl
if mtl is None: # 先找模型同目录的 mtl, 再退回脚本目录
found = sorted(glob.glob(os.path.join(obj_dir, '*.mtl'))) or \
sorted(glob.glob(os.path.join(SCRIPT_DIR, '*.mtl')))
mtl = os.path.basename(found[0]) if found else None
n_v = n_vn = 0
with open(out_path, 'w') as out, open(obj_path, 'r', errors='ignore') as src:
for line in src: # 原模型逐行原样拷贝
if line.startswith('v '):
n_v += 1
elif line.startswith('vn '):
n_vn += 1
elif line.startswith('mtllib ') and mtl:
line = 'mtllib %s\n' % mtl # 指向本目录实际存在的 mtl
elif line.startswith('usemtl ') and line.strip().endswith('.001'):
line = 'usemtl %s\n' % line.split()[1][:-4] # Blender 重命名残留
out.write(line)
# 追加包装 mesh
out.write('o Transparent\n')
out.write('s 0\n')
out.write('usemtl Transparent\n')
for p in verts_world:
out.write('v %.6f %.6f %.6f\n' % (p[0], p[1], p[2]))
base_v, base_n = n_v, n_vn
for nn in vn:
out.write('vn %.6f %.6f %.6f\n' % (nn[0], nn[1], nn[2]))
for f in wfaces:
out.write('f %d//%d %d//%d %d//%d\n' % (
f[0] + 1 + base_v, f[0] + 1 + base_n,
f[1] + 1 + base_v, f[1] + 1 + base_n,
f[2] + 1 + base_v, f[2] + 1 + base_n))
lo, hi = W.min(), W.max()
if sgn < 0:
lo, hi = -hi, -lo
print('完成! 输出: %s' % out_path)
print(' 新增 Transparent mesh: %d 顶点 / %d 面 (参考结果为 9999/19994)'
% (len(wverts), len(wfaces)))
print(' 沿视线轴 %s 的包装范围: [%.2f, %.2f]' % ('xyz'[k], lo, hi))
print(' 包装 xy 外接盒: x[%.2f, %.2f], y[%.2f, %.2f]'
% (verts_world[:, ax_u].min(), verts_world[:, ax_u].max(),
verts_world[:, ax_v].min(), verts_world[:, ax_v].max()))
if __name__ == '__main__':
main()