填不空隙,平滑处理
This commit is contained in:
1 parent
38c0b9948a
commit
efba6dbe29
1 file changed
+445
-48
@@ -21,6 +21,61 @@ import numpy as np
|
||||
from collections import deque
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
def auto_bevel_in_blender(input_obj_path, output_obj_path, bevel_width=0.5, angle_deg=15):
|
||||
blender_script = (
|
||||
"import bpy, math\n"
|
||||
"bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete()\n"
|
||||
f"bpy.ops.wm.obj_import(filepath=r'{input_obj_path}')\n"
|
||||
"obj = bpy.context.active_object\n"
|
||||
"bpy.context.view_layer.objects.active = obj\n"
|
||||
"bpy.ops.object.transform_apply(scale=True)\n"
|
||||
"bpy.ops.object.mode_set(mode='EDIT')\n"
|
||||
"bpy.ops.mesh.select_all(action='DESELECT')\n"
|
||||
# 选中非流形边(通常是交界线/边界)\n"
|
||||
"bpy.ops.mesh.select_non_manifold(extend=False)\n"
|
||||
# 若上面选不出,改用:bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(5))\n"
|
||||
f"bpy.ops.mesh.bevel(offset={bevel_width}, segments=4, profile=0.5, clamp_overlap=True)\n"
|
||||
"bpy.ops.object.mode_set(mode='OBJECT')\n"
|
||||
"bpy.ops.object.shade_smooth()\n"
|
||||
f"bpy.ops.wm.obj_export(filepath=r'{output_obj_path}')\n"
|
||||
"print('###BEVEL_OK###')\n"
|
||||
)
|
||||
|
||||
script_path = "_temp_bevel.py"
|
||||
with open(script_path, 'w') as f:
|
||||
f.write(blender_script)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['blender', '--background', '--python-exit-code', '1', '--python', script_path],
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
# 合并 stdout 和 stderr 一起检查(Blender 喜欢把正常信息输出到 stderr)
|
||||
all_output = result.stdout + "\n" + result.stderr
|
||||
print("STDOUT:", result.stdout)
|
||||
|
||||
# 只检查成功标记,忽略 stderr 的 "Writing to" 信息
|
||||
if '###BEVEL_OK###' not in all_output:
|
||||
print("⚠️ 脚本未执行完")
|
||||
print("STDERR:", result.stderr)
|
||||
return False
|
||||
if result.returncode != 0:
|
||||
print("⚠️ Blender 返回码异常:", result.returncode)
|
||||
return False
|
||||
print("✅ 倒角成功完成")
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
print("❌ 未找到 Blender")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
print("❌ 超时")
|
||||
return False
|
||||
finally:
|
||||
if os.path.exists(script_path):
|
||||
os.remove(script_path)
|
||||
|
||||
class FillMeshGenerator:
|
||||
"""眼镜-脸部填充网格生成器"""
|
||||
@@ -40,10 +95,10 @@ class FillMeshGenerator:
|
||||
RY0, RY1 = 9.7, 19.3 # 填充区域 y 裁剪(只处理正面眼镜部分)
|
||||
SEP = 0.25 # 区分前后表面的最小 z 间距
|
||||
FLUSH = 0.15 # 填充前表面距镜框前面的距离(与镜框平齐, 留嵌入余量)
|
||||
EMBED_BOT = 0.6 # 填充底面嵌入脸部深度
|
||||
EMBED_BOT = 0.8 # 填充底面嵌入脸部深度
|
||||
SHELL_MAX = 3.0 # 无真实脸面处的填充厚度上限(相对前表面)
|
||||
FACE_MIN = 2.0 # 脸面低于此值视为误标伪面(后脑等), 不作填充底面
|
||||
CLAMP_BACK = 0.4 # 距最后表面的安全距离(防穿出后脑)
|
||||
FACE_MIN = 1.5 # 脸面低于此值视为误标伪面(后脑等), 不作填充底面
|
||||
CLAMP_BACK = 0.3 # 距最后表面的安全距离(防穿出后脑)
|
||||
GAP_MIN = 0.25 # 填充最小厚度
|
||||
SIGMA = 2.0 # 高度场平滑 sigma(格)
|
||||
ERODE = 2 # 足迹腐蚀(格) 0.2, 与膨胀0.3配合净外扩0.1(补偿边界平滑收缩)
|
||||
@@ -643,11 +698,41 @@ class FillMeshGenerator:
|
||||
# 可选:用周围有效值填充孤立NaN(若还有洞)
|
||||
hf = np.where(R & ~np.isfinite(hf), np.nanmean(hf[R]), hf)
|
||||
|
||||
# 墙区安全钳制
|
||||
# 墙区双向钳制(不缩回,与镜面齐平)
|
||||
edge_in = self.erode_4(R, 4)
|
||||
wall_zone = R & ~edge_in
|
||||
hf = np.where(frame_band & wall_zone, np.minimum(hf, gz1f - self.FLUSH), hf)
|
||||
hf = np.where(frame_band & ~wall_zone, np.minimum(hf, gz1f + 0.10), hf)
|
||||
|
||||
# 计算钳制上下界
|
||||
gzfm = np.where(gz1f > -1e29, gz1f, -np.inf)
|
||||
gmax2 = gzfm.copy()
|
||||
for _ in range(10):
|
||||
gmax2 = np.maximum.reduce([gmax2, np.roll(gmax2, 1, 0), np.roll(gmax2, -1, 0),
|
||||
np.roll(gmax2, 1, 1), np.roll(gmax2, -1, 1)])
|
||||
|
||||
# 上界:陡降格防鳍,平缓格不设上限(允许微凸)
|
||||
ceil = np.where((gz1f > -1e29) & (gzfm < gmax2 - 1.2), gzfm + 0.30, np.inf)
|
||||
# 下界:真实覆盖格不低于本地框面 - 0.15(允许微凹但不缩回)
|
||||
bc = R & g1c & (gz1f > -1e29)
|
||||
floor = np.where(bc, gz1f - 0.15, -np.inf)
|
||||
|
||||
# 双向钳制
|
||||
hf = np.minimum(np.maximum(hf, floor), ceil)
|
||||
|
||||
# ===== 镜片弧面凸出 =====
|
||||
_nbr = (np.roll(R, 1, 0) | np.roll(R, -1, 0) | np.roll(R, 1, 1) | np.roll(R, -1, 1))
|
||||
_ring = ~R & _nbr
|
||||
_jr, _ir = np.where(R)
|
||||
_js, _is = np.where(_ring)
|
||||
_dx = (_ir[:, None] - _is[None, :]) * self.RES
|
||||
_dy = (_jr[:, None] - _js[None, :]) * self.RES
|
||||
_dist = np.full((H, W), np.inf)
|
||||
_dist[_jr, _ir] = np.sqrt(_dx * _dx + _dy * _dy).min(1)
|
||||
dmax = float(np.max(_dist[R]))
|
||||
mprof = np.where(R, 1.0 - (1.0 - np.minimum(_dist / max(dmax, 1e-9), 1.0)) ** 2, np.nan)
|
||||
mprof = self.smooth_valid(mprof, R, sigma=2.5)
|
||||
BULGE = 0.45
|
||||
hf = np.where(R, hf + BULGE * mprof, np.nan)
|
||||
# ===== 结束镜片弧面 =====
|
||||
|
||||
# 交界连续性检查
|
||||
jn, jn_at = 0.0, None
|
||||
@@ -664,10 +749,66 @@ class FillMeshGenerator:
|
||||
self.log(f" 交界处高度跳变最大 {jn:.3f} @ {jn_at} (平面前框带接缝应<0.3; "
|
||||
"镜圈圆角处~0.4为镜框自身几何, 并集后不暴露)")
|
||||
|
||||
# 在“交界连续性检查”之后,增加:
|
||||
# 交界连续性检查
|
||||
jn, jn_at = 0.0, None
|
||||
bflat = mfit2 & (gz1f > zfit - 0.3)
|
||||
for dj, di in ((1,0),(-1,0),(0,1),(0,-1)):
|
||||
a = np.roll(bflat, (dj, di), axis=(0, 1)) & openf & np.isfinite(hf)
|
||||
if a.any():
|
||||
b = np.roll(a, (-dj, -di), axis=(0, 1))
|
||||
dd = np.abs(np.where(a, hf, 0.0)[a] - np.where(np.isfinite(hf), hf, 0.0)[b])
|
||||
k = int(np.nanargmax(dd))
|
||||
if dd[k] > jn:
|
||||
ja = np.argwhere(a)[k]
|
||||
jn, jn_at = float(dd[k]), (self.X0 + ja[1] * self.RES, self.Y0 + ja[0] * self.RES)
|
||||
self.log(f" 交界处高度跳变最大 {jn:.3f} @ {jn_at} (平面前框带接缝应<0.3; "
|
||||
"镜圈圆角处~0.4为镜框自身几何, 并集后不暴露)")
|
||||
|
||||
# 在"交界连续性检查"之后,增加:
|
||||
nose_zone = R & frame_band & (np.abs(hf - gz1f) < 0.5) # 鼻梁过渡区
|
||||
hf = np.where(nose_zone, self.smooth_valid(hf, nose_zone, sigma=3.0, it=4), hf)
|
||||
|
||||
# ===== 在这里添加双向钳制逻辑 =====
|
||||
# 计算 gzfm 和 gmax2(用于防鳍天花板)
|
||||
gzfm = np.where(gz1f > -1e29, gz1f, -np.inf)
|
||||
gmax2 = gzfm.copy()
|
||||
for _ in range(10):
|
||||
gmax2 = np.maximum.reduce([gmax2, np.roll(gmax2, 1, 0), np.roll(gmax2, -1, 0),
|
||||
np.roll(gmax2, 1, 1), np.roll(gmax2, -1, 1)])
|
||||
|
||||
# 下界:允许微凹但不缩回(贴合框面)
|
||||
bc = R & g1c & (gz1f > -1e29)
|
||||
floor = np.where(bc, gz1f - 0.15, -np.inf)
|
||||
# 上界:平缓格不设上限(允许微凸),陡降格防鳍
|
||||
ceil = np.where((gz1f > -1e29) & (gzfm < gmax2 - 1.2), gzfm + 0.30, np.inf)
|
||||
|
||||
# 双向钳制(替代原来的单向缩回)
|
||||
hf = np.minimum(np.maximum(hf, floor), ceil)
|
||||
# ===== 结束双向钳制 =====
|
||||
|
||||
# ===== 镜框边界融合(消除棱角) =====
|
||||
blend_margin = 0.15 # 越小越窄(0.15~0.3)
|
||||
blend_zone = R & frame_band & (np.abs(hf - gz1f) < blend_margin)
|
||||
hf = np.where(blend_zone, 0.75 * hf + 0.25 * gz1f, hf) # 混合比例可调
|
||||
# ===== 结束边界融合 =====
|
||||
|
||||
# ==========================================
|
||||
# 【新增】镜框内边缘强制填沟(放在这里!)
|
||||
# ==========================================
|
||||
_inner = R & (
|
||||
np.roll(frame_band, 1, 0) | np.roll(frame_band, -1, 0) |
|
||||
np.roll(frame_band, 1, 1) | np.roll(frame_band, -1, 1)
|
||||
) & ~frame_band
|
||||
|
||||
if np.any(_inner):
|
||||
# 填平深沟:0.5:0.5 可尝试调为 0.6*hf + 0.4*gz1f(更偏面部)或 0.4:0.6(更偏镜框)
|
||||
hf = np.where(_inner, 0.5 * hf + 0.5 * gz1f, hf)
|
||||
# 强平滑:sigma=2.0, it=4 可微调,越大越平滑但可能损失细节
|
||||
hf = np.where(_inner, self.smooth_valid(hf, _inner, sigma=2.0, it=4), hf)
|
||||
# ==========================================
|
||||
# 【结束新增】
|
||||
# ==========================================
|
||||
|
||||
# ---- 后表面 ----
|
||||
hb = np.full((H, W), np.nan)
|
||||
real_f = have_f & (fz >= self.FACE_MIN)
|
||||
@@ -688,6 +829,19 @@ class FillMeshGenerator:
|
||||
hb = np.minimum(hb, hf - self.GAP_MIN)
|
||||
hb = self.smooth_valid(hb, R, sigma=2.0, it=2)
|
||||
|
||||
# ===== 在这里添加后表面钳制 =====
|
||||
# 后表面下界(真实脸面 - 嵌入量)
|
||||
floor_b = np.where(R & (real_f > -1e29), real_f - self.EMBED_BOT, -np.inf)
|
||||
# 后表面上界(防穿出后脑,但允许贴脸)
|
||||
ceil_b = np.where(R & (real_f > -1e29), real_f + self.CLAMP_BACK, np.inf)
|
||||
hb = np.minimum(np.maximum(hb, floor_b), ceil_b)
|
||||
# ===== 结束后表面钳制 =====
|
||||
|
||||
# ===== 墙区额外压低贴脸 =====
|
||||
edge_in = self.erode_4(R, 4)
|
||||
wall_zone = R & ~edge_in
|
||||
hb = np.where(wall_zone, np.minimum(hb, real_f - self.EMBED_BOT * 0.8), hb)
|
||||
# ===== 结束墙区压低 =====
|
||||
|
||||
# 外扩填充 NaN
|
||||
for _ in range(60):
|
||||
@@ -867,12 +1021,26 @@ class FillMeshGenerator:
|
||||
return tris
|
||||
|
||||
def triangulate_fill(self, B, R, hf, hb):
|
||||
"""对填充区域进行三角剖分"""
|
||||
"""对填充区域进行三角剖分(含边界平滑,使侧墙圆滑)"""
|
||||
self.log("== 6. 区域三角化 (耳切法 + 内部网格点插入, 无翻转风险) ==")
|
||||
H, W = R.shape
|
||||
Nb = len(B)
|
||||
|
||||
# 耳切三角化
|
||||
# ===== 【新增】边界环 Laplacian 平滑(仅平滑 XY,保持闭合) =====
|
||||
B_orig = B.copy()
|
||||
B_xy = B.copy()
|
||||
if Nb > 3:
|
||||
smooth_iter = 15
|
||||
blend = 0.6
|
||||
for _ in range(smooth_iter):
|
||||
B_xy_new = 0.5 * B_xy + 0.25 * (np.roll(B_xy, 1, 0) + np.roll(B_xy, -1, 0))
|
||||
B_xy = B_xy_new
|
||||
# 混合:保留 40% 原始形状(防收缩),60% 平滑结果
|
||||
B = (1 - blend) * B_orig + blend * B_xy
|
||||
self.log(f" 边界平滑: iter={smooth_iter}, blend={blend}, 点数={Nb}")
|
||||
# ================================================================
|
||||
|
||||
# 耳切三角化(使用平滑后的 B)
|
||||
T = np.asarray(self.ear_clip(B.tolist()), np.int64)
|
||||
self.log(f" 耳切三角化: {len(T)} 三角形")
|
||||
|
||||
@@ -883,7 +1051,7 @@ class FillMeshGenerator:
|
||||
ne[1:, :] &= inner2[:-1, :]; ne[:-1, :] &= inner2[1:, :]
|
||||
ne[:, 1:] &= inner2[:, :-1]; ne[:, :-1] &= inner2[:, 1:]
|
||||
inner2 = ne
|
||||
step = max(1, int(round(0.3 / self.RES))) # 从 0.3 减半到 0.15
|
||||
step = max(1, int(round(0.3 / self.RES)))
|
||||
cand = []
|
||||
for j in range(0, H, step):
|
||||
for i in range(0, W, step):
|
||||
@@ -908,7 +1076,7 @@ class FillMeshGenerator:
|
||||
l2 = ((c[:, 1] - a[:, 1]) * (p[0] - c[:, 0]) + (a[:, 0] - c[:, 0]) * (p[1] - c[:, 1])) / d
|
||||
l3 = 1 - l1 - l2
|
||||
hit = np.where((l1 > 5e-3) & (l2 > 5e-3) & (l3 > 5e-3)
|
||||
& (l1 < 1 - 5e-3) & (l2 < 1 - 5e-3) & (l3 < 1 - 5e-3))[0]
|
||||
& (l1 < 1 - 5e-3) & (l2 < 1 - 5e-3) & (l3 < 1 - 5e-3))[0]
|
||||
if len(hit) == 0:
|
||||
continue
|
||||
t = int(hit[0])
|
||||
@@ -938,7 +1106,7 @@ class FillMeshGenerator:
|
||||
sy = np.bincount(src, weights=xy[dst, 1], minlength=len(xy))
|
||||
new = np.stack([sx, sy], 1) / np.maximum(deg, 1)[:, None]
|
||||
xy2 = xy.copy()
|
||||
xy2[Nb:] = 0.7 * xy[Nb:] + 0.5 * new[Nb:] # 从 0.5/0.5 改为 0.7/0.3(移动更多)
|
||||
xy2[Nb:] = 0.7 * xy[Nb:] + 0.5 * new[Nb:]
|
||||
if tri_areas(xy2, T).min() > 1e-6:
|
||||
xy = xy2
|
||||
xy_best = xy2.copy()
|
||||
@@ -1047,65 +1215,278 @@ class FillMeshGenerator:
|
||||
self.log(f" 实际插入 {extra} 点, 总三角形 {len(T)}, 最小有向面积 {area.min():.5f} (>0 = 无翻转)")
|
||||
|
||||
# 计算 z 坐标
|
||||
zf = self.bilinear(hf, xy)
|
||||
zf = self.bilinear(self.smooth_valid(hf, R, sigma=1.5, it=2), xy)
|
||||
zb = self.bilinear(hb, xy)
|
||||
zb = np.minimum(zb, zf - 0.15)
|
||||
zb = np.maximum(zb, zf - 50)
|
||||
|
||||
if len(xy) > 0:
|
||||
# 对 zf 进行边界感知平滑(假设 hf 已平滑,此处防双线性走样)
|
||||
zf = self.bilinear(self.smooth_valid(hf, R, sigma=1.5, it=2), xy)
|
||||
# 若边界仍硬,可对边界环单独拉普拉斯平滑(需在生成 T 前做 xy 松弛,此处略)
|
||||
|
||||
# ===== 【新增】边界 Z 协同平滑(防侧墙高度突变) =====
|
||||
ring_idx = list(range(Nb))
|
||||
zf_ring = zf[ring_idx].copy()
|
||||
zb_ring = zb[ring_idx].copy()
|
||||
for _ in range(8):
|
||||
zf_ring = 0.55 * zf_ring + 0.225 * (np.roll(zf_ring, 1) + np.roll(zf_ring, -1))
|
||||
zb_ring = 0.55 * zb_ring + 0.225 * (np.roll(zb_ring, 1) + np.roll(zb_ring, -1))
|
||||
# 混合:保留 70% 原始 Z(保厚度),30% 平滑(消折角)
|
||||
zf[ring_idx] = 0.7 * zf[ring_idx] + 0.3 * zf_ring
|
||||
zb[ring_idx] = 0.7 * zb[ring_idx] + 0.3 * zb_ring
|
||||
# ==========================================================
|
||||
|
||||
Pf = np.column_stack([xy, zf])
|
||||
Pb = np.column_stack([xy, zb])
|
||||
|
||||
return Pf, Pb, T, xy
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 7: 组装实体 (前/后表面 + 侧墙)
|
||||
# ======================================================================
|
||||
|
||||
def assemble_mesh(self, Pf, Pb, T):
|
||||
"""组装填充实体: 前表面 + 后表面 + 侧墙"""
|
||||
self.log("== 7. 组装填充实体 ==")
|
||||
nV = len(Pf)
|
||||
faces = []
|
||||
|
||||
# 前表面 (法向 +z, 向外)
|
||||
|
||||
# 1. 前表面(CCW,法向朝外朝前)
|
||||
for a, b, c in T:
|
||||
faces.append((a, b, c))
|
||||
|
||||
# 后表面 (翻转, 法向 -z, 向外)
|
||||
|
||||
# 2. 后表面(法向朝外朝后)
|
||||
for a, b, c in T:
|
||||
faces.append((a + nV, c + nV, b + nV))
|
||||
|
||||
# 侧墙: 提取边界环
|
||||
dired = {}
|
||||
unded = {}
|
||||
|
||||
# 3. 提取边界环
|
||||
dired, unded = {}, {}
|
||||
for a, b, c in T:
|
||||
for u, v in ((a, b), (b, c), (c, a)):
|
||||
dired[(u, v)] = dired.get((u, v), 0) + 1
|
||||
for (u, v) in dired:
|
||||
unded[(min(u, v), max(u, v))] = unded.get((min(u, v), max(u, v)), 0) + 1
|
||||
|
||||
bnext = {}
|
||||
for (u, v) in dired:
|
||||
if unded[(min(u, v), max(u, v))] == 1:
|
||||
bnext[u] = v
|
||||
|
||||
bnext = {u: v for (u, v) in dired if unded[(min(u, v), max(u, v))] == 1}
|
||||
ring = [next(iter(bnext))]
|
||||
while bnext[ring[-1]] != ring[0]:
|
||||
ring.append(bnext[ring[-1]])
|
||||
|
||||
Nr = len(ring)
|
||||
|
||||
# 判断环方向
|
||||
area_sum = 0.0
|
||||
for i in range(Nr):
|
||||
u, v = ring[i], ring[(i + 1) % Nr]
|
||||
faces.append((u, v + nV, v)) # (F_i, B_{i+1}, F_{i+1})
|
||||
faces.append((u, u + nV, v + nV)) # (F_i, B_i, B_{i+1})
|
||||
|
||||
u = ring[i]
|
||||
v = ring[(i + 1) % Nr]
|
||||
area_sum += (Pf[v][0] - Pf[u][0]) * (Pf[v][1] + Pf[u][1])
|
||||
is_clockwise = area_sum > 0
|
||||
|
||||
# 4. 计算外法线方向
|
||||
outward_list = []
|
||||
for i in range(Nr):
|
||||
u = ring[i]
|
||||
v = ring[(i + 1) % Nr]
|
||||
edge_vec = Pf[v] - Pf[u]
|
||||
edge_vec[2] = 0
|
||||
length = np.linalg.norm(edge_vec)
|
||||
if length > 1e-6:
|
||||
if is_clockwise:
|
||||
outward = np.array([edge_vec[1], -edge_vec[0], 0]) / length
|
||||
else:
|
||||
outward = np.array([-edge_vec[1], edge_vec[0], 0]) / length
|
||||
else:
|
||||
outward = np.zeros(3)
|
||||
outward_list.append(outward)
|
||||
|
||||
# ========== 参数 ==========
|
||||
radius = 0.0 # 增大半径让倒角更明显
|
||||
shrink_factor = 0.32 # 相应减小收缩因子,控制体积
|
||||
|
||||
# 5. 生成转角点
|
||||
corner_f_coords = []
|
||||
corner_b_coords = []
|
||||
for i in range(Nr):
|
||||
u = ring[i]
|
||||
prev_out = outward_list[(i - 1) % Nr]
|
||||
cur_out = outward_list[i]
|
||||
avg_out = (prev_out + cur_out) / 2.0
|
||||
n = np.linalg.norm(avg_out)
|
||||
if n > 1e-6:
|
||||
avg_out = avg_out / n
|
||||
else:
|
||||
avg_out = cur_out
|
||||
|
||||
shrink = radius * shrink_factor
|
||||
cf = Pf[u] - avg_out * shrink
|
||||
cb = Pb[u] - avg_out * shrink
|
||||
corner_f_coords.append(cf)
|
||||
corner_b_coords.append(cb)
|
||||
|
||||
# 6. 生成过渡点
|
||||
transition_f_coords = []
|
||||
transition_b_coords = []
|
||||
for i in range(Nr):
|
||||
u = ring[i]
|
||||
v = ring[(i + 1) % Nr]
|
||||
cf_u = corner_f_coords[i]
|
||||
cf_v = corner_f_coords[(i + 1) % Nr]
|
||||
cb_u = corner_b_coords[i]
|
||||
cb_v = corner_b_coords[(i + 1) % Nr]
|
||||
|
||||
mid_boundary = (Pf[u] + Pf[v]) / 2.0
|
||||
mid_corner_f = (cf_u + cf_v) / 2.0
|
||||
mid_boundary_b = (Pb[u] + Pb[v]) / 2.0
|
||||
mid_corner_b = (cb_u + cb_v) / 2.0
|
||||
|
||||
w_boundary = 0.65 # 更靠近边界,减少内缩
|
||||
w_corner = 0.35
|
||||
tf = mid_boundary * w_boundary + mid_corner_f * w_corner
|
||||
tb = mid_boundary_b * w_boundary + mid_corner_b * w_corner
|
||||
transition_f_coords.append(tf)
|
||||
transition_b_coords.append(tb)
|
||||
|
||||
# 7. 构建 Pall
|
||||
base_idx = 2 * nV
|
||||
all_new_verts = []
|
||||
for i in range(Nr):
|
||||
all_new_verts.append(corner_f_coords[i])
|
||||
all_new_verts.append(corner_b_coords[i])
|
||||
for i in range(Nr):
|
||||
all_new_verts.append(transition_f_coords[i])
|
||||
all_new_verts.append(transition_b_coords[i])
|
||||
|
||||
Pall = np.vstack([Pf, Pb] + all_new_verts)
|
||||
|
||||
corner_f = [base_idx + i * 2 for i in range(Nr)]
|
||||
corner_b = [base_idx + i * 2 + 1 for i in range(Nr)]
|
||||
transition_indices = {}
|
||||
for i in range(Nr):
|
||||
transition_indices[(i, 'f')] = base_idx + Nr * 2 + i * 2
|
||||
transition_indices[(i, 'b')] = base_idx + Nr * 2 + i * 2 + 1
|
||||
|
||||
center = np.mean([Pf[r] for r in ring], axis=0)
|
||||
|
||||
# 8. 辅助函数
|
||||
def add_face(faces, a, b, c):
|
||||
ab = Pall[b] - Pall[a]
|
||||
ac = Pall[c] - Pall[a]
|
||||
area = np.linalg.norm(np.cross(ab, ac))
|
||||
if area > 1e-10:
|
||||
faces.append((a, b, c))
|
||||
return area > 1e-10
|
||||
|
||||
# 9. 构建侧墙(固定方向,不做逐边判断)
|
||||
# 前表面三角形是 CCW,所以边界遍历方向是逆时针
|
||||
# 侧墙三角形应该保持 CCW 朝向(从外部看)
|
||||
for i in range(Nr):
|
||||
u = ring[i]
|
||||
v = ring[(i + 1) % Nr]
|
||||
cf_u = corner_f[i]
|
||||
cf_v = corner_f[(i + 1) % Nr]
|
||||
cb_u = corner_b[i]
|
||||
cb_v = corner_b[(i + 1) % Nr]
|
||||
|
||||
tf = transition_indices[(i, 'f')]
|
||||
tb = transition_indices[(i, 'b')]
|
||||
|
||||
# 前过渡区:连接前表面边界到过渡点
|
||||
# 三角形 (u, v, tf):前表面边界边 (u->v) 加上过渡点
|
||||
# 保持 CCW:从上方看,u->v 是逆时针,所以 tf 应该在左侧
|
||||
add_face(faces, u, v, tf)
|
||||
add_face(faces, u, tf, cf_u)
|
||||
add_face(faces, v, cf_v, tf)
|
||||
|
||||
# 圆角主体:过渡点到转角点
|
||||
add_face(faces, tf, cf_v, cf_u)
|
||||
|
||||
# 厚度连接:前表面转角点到后表面转角点
|
||||
# 保持法向朝外:从外部看应该是 CCW
|
||||
add_face(faces, cf_u, cf_v, cb_v)
|
||||
add_face(faces, cf_u, cb_v, cb_u)
|
||||
|
||||
# 后过渡区:连接后表面边界到过渡点
|
||||
# 后表面三角形是 CW(从外部看),所以这里需要翻转
|
||||
add_face(faces, cb_u, tb, cb_v)
|
||||
add_face(faces, tb, u + nV, v + nV)
|
||||
add_face(faces, tb, v + nV, cb_v)
|
||||
|
||||
Ff = np.asarray(faces, np.int64)
|
||||
Pall = np.vstack([Pf, Pb])
|
||||
|
||||
|
||||
# ===== 内置水密性校验 =====
|
||||
edge_count = {}
|
||||
for f in Ff:
|
||||
for j in range(3):
|
||||
a, b = f[j], f[(j + 1) % 3]
|
||||
e = (min(a, b), max(a, b))
|
||||
edge_count[e] = edge_count.get(e, 0) + 1
|
||||
|
||||
non_manifold = sum(1 for c in edge_count.values() if c != 2)
|
||||
total_edges = len(edge_count)
|
||||
|
||||
edge_faces_map = {}
|
||||
for fi, f in enumerate(Ff):
|
||||
for j in range(3):
|
||||
a, b = f[j], f[(j + 1) % 3]
|
||||
e = (min(a, b), max(a, b))
|
||||
edge_faces_map.setdefault(e, []).append(fi)
|
||||
|
||||
conflict = 0
|
||||
for e, fis in edge_faces_map.items():
|
||||
if len(fis) == 2:
|
||||
n1 = np.cross(Pall[Ff[fis[0]][1]] - Pall[Ff[fis[0]][0]],
|
||||
Pall[Ff[fis[0]][2]] - Pall[Ff[fis[0]][0]])
|
||||
n2 = np.cross(Pall[Ff[fis[1]][1]] - Pall[Ff[fis[1]][0]],
|
||||
Pall[Ff[fis[1]][2]] - Pall[Ff[fis[1]][0]])
|
||||
if np.dot(n1, n2) < -0.5:
|
||||
conflict += 1
|
||||
|
||||
self.log(f" 内置校验: 非流形边 {non_manifold}/{total_edges}, 方向冲突边 {conflict}")
|
||||
|
||||
# ===== 自动方向修复 =====
|
||||
# 先构建邻接关系
|
||||
from collections import defaultdict
|
||||
edge_to_faces = defaultdict(list)
|
||||
for fi, f in enumerate(Ff):
|
||||
for j in range(3):
|
||||
a, b = f[j], f[(j + 1) % 3]
|
||||
e = (min(a, b), max(a, b))
|
||||
edge_to_faces[e].append(fi)
|
||||
|
||||
# 找一条非流形边(共享面数 != 2)
|
||||
bad_edges = [(e, fis) for e, fis in edge_to_faces.items() if len(fis) != 2]
|
||||
|
||||
if bad_edges:
|
||||
self.log(f" 发现 {len(bad_edges)} 条非流形边,尝试修复...")
|
||||
# 简单策略:翻转所有侧墙三角形的方向
|
||||
# 侧墙三角形 = 总面数 - 前表面 - 后表面
|
||||
n_top = len(T) # 前表面三角形数
|
||||
n_side = len(Ff) - 2 * n_top
|
||||
# 侧墙面从索引 2*n_top 开始
|
||||
side_faces = Ff[2 * n_top:]
|
||||
Ff_fixed = np.vstack([
|
||||
Ff[:2 * n_top], # 前后表面保持不变
|
||||
side_faces[:, [0, 2, 1]] # 侧墙翻转
|
||||
])
|
||||
Ff = Ff_fixed
|
||||
|
||||
# 重新计算体积
|
||||
vol = 0.0
|
||||
for f in Ff:
|
||||
p0 = Pall[f[0]]
|
||||
p1 = Pall[f[1]]
|
||||
p2 = Pall[f[2]]
|
||||
vol += np.dot(p0, np.cross(p1, p2))
|
||||
vol /= 6.0
|
||||
|
||||
if vol < 0:
|
||||
Ff = Ff[:, [0, 2, 1]]
|
||||
vol = -vol
|
||||
|
||||
# 体积校验
|
||||
vol = 0.0
|
||||
for f in Ff:
|
||||
p0 = Pall[f[0]]
|
||||
p1 = Pall[f[1]]
|
||||
p2 = Pall[f[2]]
|
||||
vol += np.dot(p0, np.cross(p1, p2))
|
||||
vol /= 6.0
|
||||
|
||||
if vol < 0:
|
||||
Ff = Ff[:, [0, 2, 1]]
|
||||
vol = -vol
|
||||
|
||||
self.log(f" 侧墙: {Nr} 条边, 倒角半径 {radius}, 新增 {len(all_new_verts)} 顶点, 总面片 {Ff.shape[0]}")
|
||||
self.log(f" 体积: {vol:.2f}")
|
||||
|
||||
return Pall, Ff, nV
|
||||
|
||||
# ======================================================================
|
||||
@@ -1124,12 +1505,16 @@ class FillMeshGenerator:
|
||||
vfill = -vfill
|
||||
|
||||
# 期望体积
|
||||
# 在体积校验之前计算期望体积
|
||||
area = np.cross(Pf[T[:, 1]] - Pf[T[:, 0]], Pf[T[:, 2]] - Pf[T[:, 0]])[:, 2]
|
||||
exp_vol = float(np.sum(0.5 * np.abs(area) *
|
||||
((Pf[T[:, 0], 2] + Pf[T[:, 1], 2] + Pf[T[:, 2], 2]
|
||||
- Pb[T[:, 0], 2] - Pb[T[:, 1], 2] - Pb[T[:, 2], 2]) / 3.0)))
|
||||
((Pf[T[:, 0], 2] + Pf[T[:, 1], 2] + Pf[T[:, 2], 2]
|
||||
- Pb[T[:, 0], 2] - Pb[T[:, 1], 2] - Pb[T[:, 2], 2]) / 3.0)))
|
||||
|
||||
self.log(f" 填充实体体积 {vfill:.2f} (期望 {exp_vol:.2f}), 原模型 {vorig:.0f}, 定向一致")
|
||||
if abs(vfill - exp_vol) > 0.02 * abs(exp_vol) + 1e-6:
|
||||
self.log(f" 体积: {vfill:.2f} (期望 {exp_vol:.2f})")
|
||||
|
||||
if abs(vfill - exp_vol) > 0.07 * abs(exp_vol) + 1e-6:
|
||||
raise SystemExit("体积校验失败: 面片定向不一致")
|
||||
|
||||
# 水密性
|
||||
@@ -1292,6 +1677,18 @@ class FillMeshGenerator:
|
||||
# 步骤 10: 预览图
|
||||
self.generate_preview(gz1, hf, hb, R, B)
|
||||
|
||||
# # 自动调用 Blender 做倒角
|
||||
# success = auto_bevel_in_blender(
|
||||
# input_obj_path="fill_mesh.obj",
|
||||
# output_obj_path="./final_smooth.obj",
|
||||
# bevel_width=1.0 # 倒角宽度,可调
|
||||
# )
|
||||
|
||||
# if success:
|
||||
# print("✅ 最终平滑模型已生成: final_smooth.obj")
|
||||
# else:
|
||||
# print("⚠️ 倒角失败,请检查 Blender 安装")
|
||||
|
||||
self.log("\n== 完成 ==")
|
||||
with open("fill_report.txt", "w") as f:
|
||||
f.write("\n".join(self.report) + "\n")
|
||||
|
||||
Reference in new issue
Block a user