Files
orisys-sensor-design/blender/scripts/patch_geometry.py
T
ykcheungab 267334d543 fix cli redundant images
finger-xynova first design completed
2026-09-28 16:18:17 +08:00

150 lines
6.5 KiB
Python

"""Camera-cell clipping without UV topology assumptions (stdlib only).
Attributes are interpolated in homogeneous space before perspective division,
then as barycentric/w and 1/w in screen space. UVs are resolved only against
one evaluated loop triangle; neither island joining nor periodic wrapping occurs.
Visibility is sampled, not an exact occluder polygon arrangement. A fragment
whose seven probes (vertices, edge midpoints, centroid) all agree is emitted or
dropped as one unit; occluders smaller than the probe spacing can therefore be
missed inside such a fragment. Only mixed fragments — one straddling an
occlusion boundary — are refined, down to MIXED_SPAN_CELLS (1/4 cell) or depth
6, and an unresolved mixed leaf is rejected rather than guessed, so occluded
surface is left white. Seven probes per leaf are NOT a proof of full visibility.
"""
import math
# Screen-space tolerance for resolving a fragment that straddles an occlusion
# boundary, expressed as a fraction of one camera grid cell. The boundary is a
# curve, so cost grows as 1/tolerance; 1/4 cell already resolves the silhouette
# to a few output pixels at production raster size, while 1/128 cell (sub-pixel)
# multiplied polygon count by ~6 for no visible gain.
MIXED_SPAN_CELLS = 1 / 4
def clip_halfspace(points, distance):
if not points:
return []
result = []
prev = points[-1]
dp = distance(prev)
for cur in points:
dc = distance(cur)
if (dp >= 0) != (dc >= 0):
t = dp / (dp - dc)
result.append(tuple(a + t * (b - a) for a, b in zip(prev, cur)))
if dc >= 0:
result.append(cur)
prev, dp = cur, dc
return result
def clip_rect(points, x0, x1, y0, y1):
for axis, bound, sign in ((0, x0, 1), (0, x1, -1), (1, y0, 1), (1, y1, -1)):
# Bind the loop variables as defaults: the callback must clip against the
# plane of *this* iteration, not whatever the loop left behind.
points = clip_halfspace(
points,
lambda p, axis=axis, bound=bound, sign=sign: sign * (p[axis] - bound),
)
return points
def area(points):
return abs(sum(a[0] * b[1] - b[0] * a[1]
for a, b in zip(points, points[1:] + points[:1]))) / 2
def project_triangle(clip_vertices):
"""Clip against all six camera planes, return (u,v,1/w,b0/w,b1/w,b2/w)."""
points = [tuple(v) + tuple(float(i == j) for j in range(3))
for i, v in enumerate(clip_vertices)]
for axis in range(3):
for sign in (-1, 1):
# Same late-binding hazard as clip_rect: bind axis/sign per iteration so
# the clip plane cannot be read from a later loop state.
points = clip_halfspace(
points,
lambda p, axis=axis, sign=sign: p[3] + sign * p[axis],
)
return [((p[0] / p[3] + 1) / 2, (p[1] / p[3] + 1) / 2,
1 / p[3], *(b / p[3] for b in p[4:]))
for p in points if p[3] > 1e-15]
def barycentric(point):
return tuple(v / point[2] for v in point[3:6])
def interpolate_uv(point, loop_uvs):
weights = barycentric(point)
return tuple(sum(w * uv[k] for w, uv in zip(weights, loop_uvs)) for k in range(2))
def mean(*points):
return tuple(sum(values) / len(points) for values in zip(*points))
def visible_triangles(triangle, visible, nx, ny, edge_sample_grid=5, depth=0,
fill_unresolved=False):
a, b, c = triangle
ab, bc, ca = mean(a, b), mean(b, c), mean(c, a)
probes = (a, b, c, ab, bc, ca, mean(a, b, c))
states = [visible(barycentric(p)) for p in probes]
span = max((max(p[k] for p in triangle) - min(p[k] for p in triangle)) * n
for k, n in ((0, nx), (1, ny)))
# A uniformly visible fragment is emitted as-is. Subdividing it would only
# split an area that already shares one cell colour, multiplying polygon
# count by ~4**3 per fragment for no visual or vector benefit. Only mixed
# fragments (an occlusion boundary crossing them) are refined, because only
# there does a sub-fragment disagree with its neighbours.
if all(states):
yield triangle
return
if not any(states):
return
if depth >= 6 or span <= MIXED_SPAN_CELLS:
# Default: never accept an unresolved mixed leaf, even if its centroid is
# visible, so occluded surface is left white rather than guessed.
#
# ``fill_unresolved`` accepts the leaf instead. On this surface the only occluder
# is the surface itself, and it intrudes only near the silhouette, where an
# unpainted leaf is a hole in the printed marker while a slight stretch is not.
# The default stays conservative so the single-view artifacts do not change.
if all(states) or fill_unresolved:
yield triangle
return
children = []
for child in ((a, ab, ca), (ab, b, bc), (ca, bc, c), (ab, bc, ca)):
children.extend(visible_triangles(child, visible, nx, ny, edge_sample_grid,
depth + 1, fill_unresolved))
# Coalesce only the exact parent triangle after every child was accepted.
# This is not a UV hull or a cross-triangle/island join.
if children and abs(sum(area(list(t)) for t in children) - area(list(triangle))) <= area(list(triangle)) * 1e-12:
yield triangle
else:
yield from children
def cell_fragments(clip_vertices, nx, ny, visible, edge_sample_grid=5,
fill_unresolved=False):
"""Yield (column,row,screen polygon) fragments of ONE loop triangle."""
projected = project_triangle(clip_vertices)
if len(projected) < 3 or area(projected) < 1e-18:
return
i0 = max(0, math.floor(min(p[0] for p in projected) * nx))
i1 = min(nx - 1, math.floor(max(p[0] for p in projected) * nx))
j0 = max(0, math.floor(min(p[1] for p in projected) * ny))
j1 = min(ny - 1, math.floor(max(p[1] for p in projected) * ny))
for j in range(j0, j1 + 1):
for i in range(i0, i1 + 1):
polygon = clip_rect(projected, i / nx, (i + 1) / nx, j / ny, (j + 1) / ny)
if len(polygon) < 3 or area(polygon) < 1e-18:
continue
for k in range(1, len(polygon) - 1):
tri = (polygon[0], polygon[k], polygon[k + 1])
if area(list(tri)) > 1e-18:
for fragment in visible_triangles(tri, visible, nx, ny, edge_sample_grid,
0, fill_unresolved):
yield i, j, fragment