"""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)): points = clip_halfspace(points, lambda p: 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): points = clip_halfspace(points, lambda p: 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): 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: # Never accept an unresolved mixed leaf, even if its centroid is visible. if all(states): 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)) # 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): """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): yield i, j, fragment