fix cli redundant images

finger-xynova first design completed
This commit is contained in:
ykcheungab
2026-09-28 16:18:17 +08:00
parent 88922f21ef
commit 267334d543
40 changed files with 618899 additions and 263563 deletions
+135 -3
View File
@@ -1,8 +1,57 @@
"""Blender adapter for evaluated, camera-local loop-triangle fragments."""
from patch_geometry import cell_fragments, interpolate_uv
import math
from patch_geometry import barycentric, cell_fragments, interpolate_uv
def build_patch_geometry(obj, cam, scene, depsgraph, uv_names, nx, ny, edge_sample_grid):
def _polygon_area_3d(points):
"""Area of a planar polygon given its vertices in order (fan from vertex 0)."""
if len(points) < 3:
return 0.0
total = 0.0
for k in range(1, len(points) - 1):
total += (points[k] - points[0]).cross(points[k + 1] - points[0]).length
return total * 0.5
def _point_segment_distance(point, a, b):
"""Shortest 3D distance from ``point`` to the segment ``a``-``b``."""
ab = b - a
denom = ab.dot(ab)
if denom <= 0.0:
return (point - a).length
t = (point - a).dot(ab) / denom
t = 0.0 if t < 0.0 else (1.0 if t > 1.0 else t)
return (point - (a + ab * t)).length
def _min_seam_distance(point, segments):
"""Shortest distance from ``point`` to any of the seam segments."""
best = None
for a, b in segments:
d = _point_segment_distance(point, a, b)
if best is None or d < best:
best = d
return best if best is not None else float("inf")
def build_patch_geometry(obj, cam, scene, depsgraph, uv_names, nx, ny, edge_sample_grid,
fill_unresolved_leaves=False, seam_segments=None,
seam_search_mm=0.0):
"""Evaluated loop-triangle fragments for one camera, per UV target.
``fill_unresolved_leaves`` accepts fragments whose visibility could not be resolved at
the finest subdivision instead of leaving them unpainted. It trades a slight stretch at
silhouette-adjacent leaves for a marker with no holes; the default is the conservative
behaviour, so single-view artifacts stay byte-identical.
``seam_segments`` is the authored seam as world-space ``(a, b)`` pairs. When given, every
fragment is measured against it and the distance is reported as
``fragment_seam_distance_mm``, which is what the seam margin filters on. Only triangles
whose nearest vertex is within ``seam_search_mm`` plus the triangle's own radius are
measured exactly; everything else is reported as ``inf`` (it cannot be inside a margin of
that size, and the exact number would be wasted work on the whole surface).
"""
from mathutils import Vector
if cam.data.type not in {'PERSP', 'ORTHO'}:
raise ValueError('Patch projection requires a perspective or orthographic camera')
@@ -24,6 +73,30 @@ def build_patch_geometry(obj, cam, scene, depsgraph, uv_names, nx, ny, edge_samp
forward = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
output = {name: {'mesh': [], 'patches': []} for name in uv_names}
ray_count = 0
# Triangle-level and fragment-level records, shared by every UV target: the
# fragment walk is identical for all of them (they differ only in which layer
# the barycentrics are resolved against), so recording it per target would be
# the same numbers repeated. Consumed by ownership assignment; the single-view
# path never reads them.
triangle_vertices = []
triangle_area_mm2 = []
fragment_triangle = []
fragment_incidence_deg = []
fragment_footprint_mm2 = []
fragment_seam_distance_mm = []
triangle_seam_distance_mm = []
if seam_segments:
# Per-vertex distance to the seam polyline, computed once. A fragment's distance is
# then only worth measuring exactly where its triangle can plausibly reach the
# margin, which keeps this off the whole surface.
vertex_seam_distance = [
_min_seam_distance(evaluated.matrix_world @ vertex.co, seam_segments)
for vertex in mesh.vertices
]
else:
vertex_seam_distance = None
for triangle in mesh.loop_triangles:
world = [evaluated.matrix_world @ mesh.vertices[i].co for i in triangle.vertices]
clips = [tuple(clip_matrix @ p.to_4d()) for p in world]
@@ -31,6 +104,35 @@ def build_patch_geometry(obj, cam, scene, depsgraph, uv_names, nx, ny, edge_samp
for name, layer in layers.items()}
for name in uv_names:
output[name]['mesh'].append(uvs[name])
triangle_vertices.append(tuple(triangle.vertices))
triangle_area_mm2.append(_polygon_area_3d(world))
if vertex_seam_distance is not None:
triangle_near = min(vertex_seam_distance[i] for i in triangle.vertices)
node = sum(world, Vector()) / 3.0
radius = max((p - node).length for p in world)
triangle_seam_distance_mm.append(triangle_near)
else:
triangle_near = radius = None
# Incidence of this triangle to the camera. ``abs`` on purpose: the mesh
# carries inward-facing normals on most faces, and ownership cares about the
# angle between the surface and the view ray, not about winding. Whether the
# winding is a defect is reported separately, not decided here.
normal = (world[1] - world[0]).cross(world[2] - world[0])
centroid = (world[0] + world[1] + world[2]) / 3.0
if normal.length > 1e-15:
normal.normalize()
to_camera = (origin - centroid) if camera.data.type == 'PERSP' else -forward
if to_camera.length > 1e-15:
cosine = min(1.0, max(0.0, abs(normal.dot(to_camera.normalized()))))
incidence_deg = math.degrees(math.acos(cosine))
else:
incidence_deg = 90.0
else:
incidence_deg = 90.0
cache = {}
# ``cache`` and ``world`` are bound as defaults: ``cache`` is rebound
@@ -62,10 +164,40 @@ def build_patch_geometry(obj, cam, scene, depsgraph, uv_names, nx, ny, edge_samp
cache[key] = result
return result
for i, j, fragment in cell_fragments(clips, nx, ny, visible, edge_sample_grid):
for i, j, fragment in cell_fragments(clips, nx, ny, visible, edge_sample_grid,
fill_unresolved_leaves):
# Fragments are triangles (cell_fragments fans and refines to triples),
# so the surface footprint is exact, not an approximation.
points = [sum((p * w for p, w in zip(world, barycentric(p))), Vector())
for p in fragment]
fragment_triangle.append(triangle.index)
fragment_incidence_deg.append(incidence_deg)
fragment_footprint_mm2.append(_polygon_area_3d(points))
if vertex_seam_distance is not None:
if triangle_near <= seam_search_mm + radius:
fragment_seam_distance_mm.append(
min(_min_seam_distance(point, seam_segments) for point in points)
)
else:
# Cannot be inside any margin this size; skip the exact measurement.
fragment_seam_distance_mm.append(float("inf"))
for name in uv_names:
polygon = [interpolate_uv(p, uvs[name]) for p in fragment]
output[name]['patches'].append((i, j, polygon))
output['_meta'] = {
'triangle_count': len(mesh.loop_triangles),
'triangle_vertices': triangle_vertices,
'triangle_area_mm2': triangle_area_mm2,
'fragment_triangle': fragment_triangle,
'fragment_incidence_deg': fragment_incidence_deg,
'fragment_footprint_mm2': fragment_footprint_mm2,
# None (rather than a list) when no seam was supplied, so a consumer can tell
# "not measured" from "measured and far away" without guessing.
'fragment_seam_distance_mm': fragment_seam_distance_mm or None,
'triangle_seam_distance_mm': triangle_seam_distance_mm or None,
'camera_type': camera.data.type,
}
print(f'Evaluated {len(mesh.loop_triangles)} loop triangles; {ray_count} visibility rays; '
f'{len(output[uv_names[0]]["patches"])} local fragments per UV target')
print('Visibility sampling: seven probes per fragment; uniformly probed '