fixed xynova texture generation problem
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user