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
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Blender adapter for evaluated, camera-local loop-triangle fragments."""
|
||||
from patch_geometry import cell_fragments, interpolate_uv
|
||||
|
||||
|
||||
def build_patch_geometry(obj, cam, scene, depsgraph, uv_names, nx, ny, edge_sample_grid):
|
||||
from mathutils import Vector
|
||||
if cam.data.type not in {'PERSP', 'ORTHO'}:
|
||||
raise ValueError('Patch projection requires a perspective or orthographic camera')
|
||||
camera = cam.evaluated_get(depsgraph)
|
||||
evaluated = obj.evaluated_get(depsgraph)
|
||||
mesh = evaluated.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||
try:
|
||||
mesh.calc_loop_triangles()
|
||||
layers = {name: mesh.uv_layers.get(name) for name in uv_names}
|
||||
if any(layer is None for layer in layers.values()):
|
||||
raise ValueError('An evaluated mesh UV layer is missing')
|
||||
render = scene.render
|
||||
projection = camera.calc_matrix_camera(
|
||||
depsgraph, x=render.resolution_x, y=render.resolution_y,
|
||||
scale_x=render.pixel_aspect_x, scale_y=render.pixel_aspect_y)
|
||||
view = camera.matrix_world.normalized().inverted()
|
||||
clip_matrix = projection @ view
|
||||
origin = camera.matrix_world.translation
|
||||
forward = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
output = {name: {'mesh': [], 'patches': []} for name in uv_names}
|
||||
ray_count = 0
|
||||
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]
|
||||
uvs = {name: [tuple(layer.data[i].uv) for i in triangle.loops]
|
||||
for name, layer in layers.items()}
|
||||
for name in uv_names:
|
||||
output[name]['mesh'].append(uvs[name])
|
||||
cache = {}
|
||||
|
||||
def visible(weights):
|
||||
nonlocal ray_count
|
||||
key = tuple(round(w, 12) for w in weights)
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
point = sum((p * w for p, w in zip(world, weights)), Vector())
|
||||
depth = -(view @ point).z
|
||||
ray_origin = origin if camera.data.type == 'PERSP' else point - forward * depth
|
||||
direction = point - ray_origin
|
||||
distance = direction.length
|
||||
# Relative hit-position tolerance in world units; never rely on
|
||||
# original/evaluated polygon indices (modifiers can change them).
|
||||
tolerance = max(1e-7, distance * 2e-5)
|
||||
if distance <= tolerance:
|
||||
cache[key] = False
|
||||
return False
|
||||
direction.normalize()
|
||||
hit, location, normal, index, hit_obj, matrix = scene.ray_cast(
|
||||
depsgraph, ray_origin, direction, distance=distance + tolerance)
|
||||
ray_count += 1
|
||||
same_object = hit_obj is not None and getattr(hit_obj, 'original', hit_obj) == obj.original
|
||||
result = bool(hit and same_object and (location - point).length <= tolerance)
|
||||
cache[key] = result
|
||||
return result
|
||||
|
||||
for i, j, fragment in cell_fragments(clips, nx, ny, visible, edge_sample_grid):
|
||||
for name in uv_names:
|
||||
polygon = [interpolate_uv(p, uvs[name]) for p in fragment]
|
||||
output[name]['patches'].append((i, j, polygon))
|
||||
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(f'Visibility sampling: seven probes per fragment; uniformly probed '
|
||||
f'fragments are accepted or dropped whole, mixed boundary fragments '
|
||||
f'refine to 1/4 cell or depth 6 and unresolved leaves are rejected. '
|
||||
f'Sub-sample occluders may be missed; hit tolerance max(1e-7, distance*2e-5).')
|
||||
return output
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
@@ -33,6 +33,11 @@ import bpy
|
||||
import numpy as np
|
||||
from mathutils import Vector
|
||||
|
||||
# Blender -P does not consistently include the script directory on sys.path.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from patch_geometry import clip_rect
|
||||
from patch_projection import build_patch_geometry
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Common Utilities
|
||||
# ----------------------------------------------------------------------
|
||||
@@ -522,6 +527,20 @@ def generate_valid_color(neighbors, r):
|
||||
color[channel] = sampled
|
||||
return color
|
||||
|
||||
def precompute_cell_colors(nx, ny, r):
|
||||
"""Assign every seeded grid cell once, independent of geometry/UV visibility."""
|
||||
colors = np.zeros((ny, nx, 3), dtype=np.float32)
|
||||
for j in range(ny):
|
||||
for i in range(nx):
|
||||
neighbors = []
|
||||
if j > 0:
|
||||
neighbors.append(colors[j - 1, i])
|
||||
if i > 0:
|
||||
neighbors.append(colors[j, i - 1])
|
||||
colors[j, i] = generate_valid_color(neighbors, r) if neighbors else np.random.rand(3)
|
||||
return colors
|
||||
|
||||
|
||||
def checker_patch_color(i, j):
|
||||
"""Return a black/white checker color for patch index (i, j)."""
|
||||
if (i + j) % 2 == 0:
|
||||
@@ -848,7 +867,7 @@ def generate_patch_texture(
|
||||
vector_width_mm=164.0, vector_height_mm=160.0,
|
||||
uv_centre=None, cross_length=40,
|
||||
cross_thickness=3, cross_color=(1,0,0,1), edge_sample_grid=5,
|
||||
uv_layer=None, image_name="PatchTexture"
|
||||
uv_layer=None, image_name="PatchTexture", patch_colors=None, geometry=None
|
||||
):
|
||||
if not output_path:
|
||||
raise ValueError("output_path is required (absolute PNG path)")
|
||||
@@ -856,50 +875,22 @@ def generate_patch_texture(
|
||||
_warn_if_page_aspect_differs(
|
||||
texture_width, texture_height, vector_width_mm, vector_height_mm
|
||||
)
|
||||
cam_data = cam.data
|
||||
render = scene.render
|
||||
res_x = render.resolution_x
|
||||
res_y = render.resolution_y
|
||||
if cam_data.sensor_fit == 'HORIZONTAL':
|
||||
sensor_width = cam_data.sensor_width
|
||||
sensor_height = sensor_width / (res_x / res_y)
|
||||
else:
|
||||
sensor_height = cam_data.sensor_height
|
||||
sensor_width = sensor_height * (res_x / res_y)
|
||||
focal = cam_data.lens
|
||||
frame_width = sensor_width / focal
|
||||
frame_height = sensor_height / focal
|
||||
if uv_layer is None:
|
||||
uv_layer = obj.data.uv_layers.active
|
||||
if uv_layer is None:
|
||||
raise ValueError("A UV layer is required")
|
||||
if patch_colors is None:
|
||||
patch_colors = precompute_cell_colors(nx, ny, r)
|
||||
if geometry is None:
|
||||
geometry = build_patch_geometry(
|
||||
obj, cam, scene, depsgraph, [uv_layer.name], nx, ny, edge_sample_grid
|
||||
)[uv_layer.name]
|
||||
|
||||
edge_sample_grid = max(2, int(edge_sample_grid))
|
||||
patch_uv_polygons = []
|
||||
patch_colors = np.zeros((ny, nx, 3), dtype=np.float32)
|
||||
skipped_patches = 0
|
||||
|
||||
print(f"Raycasting {nx*ny} patches...")
|
||||
for j in range(ny):
|
||||
v_start = j / ny
|
||||
v_end = (j+1) / ny
|
||||
for i in range(nx):
|
||||
u_start = i / nx
|
||||
u_end = (i+1) / nx
|
||||
uv_poly = sample_patch_uv_polygon(
|
||||
scene, depsgraph, cam, obj,
|
||||
u_start, u_end, v_start, v_end,
|
||||
frame_width, frame_height,
|
||||
edge_sample_grid=edge_sample_grid, uv_layer=uv_layer
|
||||
)
|
||||
if uv_poly is None:
|
||||
patch_uv_polygons.append(None)
|
||||
skipped_patches += 1
|
||||
continue
|
||||
patch_uv_polygons.append(uv_poly)
|
||||
|
||||
neighbors = []
|
||||
if j > 0: neighbors.append(patch_colors[j-1, i])
|
||||
if i > 0: neighbors.append(patch_colors[j, i-1])
|
||||
color = np.random.rand(3) if len(neighbors)==0 else generate_valid_color(neighbors, r)
|
||||
patch_colors[j, i] = color
|
||||
print(f"Skipped {skipped_patches} patches with fewer than 3 mesh samples.")
|
||||
def pixel_polygon(uv_polygon):
|
||||
# Authored per-loop UVs are authoritative, including wide/mirrored islands.
|
||||
# Clip to this output tile; do not infer a periodic seam or draw copies.
|
||||
return [(u * (texture_width - 1), v * (texture_height - 1))
|
||||
for u, v in clip_rect(uv_polygon, 0.0, 1.0, 0.0, 1.0)]
|
||||
|
||||
# Create image – start with black background
|
||||
img_name = image_name
|
||||
@@ -913,9 +904,8 @@ def generate_patch_texture(
|
||||
|
||||
# Rasterize all mesh UVs in white (visible + non-visible parts). Retain
|
||||
# the exact same polygons for native SVG/PDF output.
|
||||
mesh_polygons = mesh_uv_pixel_polygons(
|
||||
obj, texture_width, texture_height, uv_layer=uv_layer
|
||||
)
|
||||
mesh_polygons = [pixel_polygon(poly) for poly in geometry['mesh']]
|
||||
mesh_polygons = [poly for poly in mesh_polygons if len(poly) >= 3]
|
||||
for pixel_poly in mesh_polygons:
|
||||
fill_polygon(
|
||||
None, pixels, texture_width, texture_height,
|
||||
@@ -924,16 +914,14 @@ def generate_patch_texture(
|
||||
|
||||
# Draw the visible patches and retain each editable vector shape.
|
||||
patch_shapes = []
|
||||
idx = 0
|
||||
for j in range(ny):
|
||||
for i in range(nx):
|
||||
uv_poly = patch_uv_polygons[idx]; idx += 1
|
||||
if uv_poly is None: continue
|
||||
color = patch_colors[j,i]
|
||||
rgba = (color[0], color[1], color[2], 1.0)
|
||||
for pixel_poly in uv_polygon_to_pixel_polygons(uv_poly, texture_width, texture_height):
|
||||
patch_shapes.append((pixel_poly, rgba))
|
||||
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||
for i, j, uv_poly in geometry['patches']:
|
||||
color = patch_colors[j, i]
|
||||
rgba = (color[0], color[1], color[2], 1.0)
|
||||
pixel_poly = pixel_polygon(uv_poly)
|
||||
if len(pixel_poly) < 3:
|
||||
continue
|
||||
patch_shapes.append((pixel_poly, rgba))
|
||||
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||
|
||||
cross_shapes = []
|
||||
if uv_centre is not None:
|
||||
@@ -1608,13 +1596,17 @@ def main():
|
||||
if config["seed"] is not None:
|
||||
np.random.seed(config["seed"])
|
||||
print(f"Using colour seed {config['seed']}")
|
||||
# One RNG snapshot is replayed for every target, so the same physical patch
|
||||
# keeps the same colour in each UV layout (and reruns reproduce when seeded).
|
||||
rng_state = np.random.get_state()
|
||||
|
||||
# Both targets share the very same cell table and visibility-clipped fragments.
|
||||
patch_colors = precompute_cell_colors(config['nx'], config['ny'], config['color_r'])
|
||||
start_time = time.time()
|
||||
try:
|
||||
geometry = build_patch_geometry(
|
||||
obj, cam, scene, depsgraph, [name for name, _, _ in targets],
|
||||
config['nx'], config['ny'], config['edge_sample_grid'])
|
||||
except Exception as exc:
|
||||
print(f'ERROR: patch geometry failed: {exc}', file=sys.stderr)
|
||||
return 1
|
||||
for uv_map, artifacts, uv_layer in targets:
|
||||
np.random.set_state(rng_state)
|
||||
uv_centre = _uv_centre(uv_layer) if config["cross_enabled"] else None
|
||||
print(f"--- UV map {uv_map!r}")
|
||||
try:
|
||||
@@ -1634,6 +1626,7 @@ def main():
|
||||
edge_sample_grid=config["edge_sample_grid"],
|
||||
uv_layer=uv_layer,
|
||||
image_name=f"PatchTexture_{uv_map}",
|
||||
patch_colors=patch_colors, geometry=geometry[uv_map],
|
||||
)
|
||||
except Exception as exc:
|
||||
print(
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Run with python -m unittest discover -s blender/scripts -p test_patch_geometry.py."""
|
||||
import unittest
|
||||
from patch_geometry import (area, barycentric, cell_fragments, clip_rect,
|
||||
interpolate_uv, project_triangle, visible_triangles)
|
||||
|
||||
|
||||
class GeometryTests(unittest.TestCase):
|
||||
def test_perspective_correct(self):
|
||||
points = project_triangle([(-1, -1, 0, 1), (2, -2, 0, 2), (-4, 4, 0, 4)])
|
||||
p = tuple((a + b + c) / 3 for a, b, c in zip(*points))
|
||||
self.assertAlmostEqual(sum(barycentric(p)), 1)
|
||||
uv = interpolate_uv(p, [(0, 0), (1, 0), (0, 1)])
|
||||
self.assertAlmostEqual(uv[0], 2 / 7)
|
||||
self.assertAlmostEqual(uv[1], 1 / 7)
|
||||
|
||||
def test_quad_both_loop_triangles_cover_square(self):
|
||||
corners = [(-1, -1, 0, 1), (1, -1, 0, 1), (1, 1, 0, 1), (-1, 1, 0, 1)]
|
||||
fragments = []
|
||||
for indices in ((0, 1, 2), (0, 2, 3)):
|
||||
fragments.extend(cell_fragments([corners[k] for k in indices], 3, 2, lambda _: True))
|
||||
self.assertAlmostEqual(sum(area(list(p)) for _, _, p in fragments), 1)
|
||||
for i, j, poly in fragments:
|
||||
self.assertTrue(all(i / 3 - 1e-12 <= p[0] <= (i + 1) / 3 + 1e-12 for p in poly))
|
||||
self.assertTrue(all(j / 2 - 1e-12 <= p[1] <= (j + 1) / 2 + 1e-12 for p in poly))
|
||||
|
||||
def test_frustum_and_near_clipping(self):
|
||||
poly = project_triangle([(-2, -1, -2, 1), (1, -1, 0, 1), (0, 2, 0, 1)])
|
||||
self.assertGreater(len(poly), 2)
|
||||
for p in poly:
|
||||
self.assertTrue(0 <= p[0] <= 1 and 0 <= p[1] <= 1)
|
||||
self.assertAlmostEqual(sum(barycentric(p)), 1)
|
||||
self.assertTrue(all(w >= -1e-12 for w in barycentric(p)))
|
||||
|
||||
def test_behind_camera_is_empty(self):
|
||||
self.assertEqual(project_triangle([(-1, -1, 0, -1), (1, -1, 0, -1), (0, 1, 0, -1)]), [])
|
||||
|
||||
def test_wide_uv_triangle_does_not_wrap(self):
|
||||
poly = project_triangle([(-1, -1, 0, 1), (1, -1, 0, 1), (-1, 1, 0, 1)])
|
||||
uvs = [(0.02, 0.1), (0.98, 0.1), (0.02, 0.9)]
|
||||
self.assertEqual([interpolate_uv(p, uvs) for p in poly], uvs)
|
||||
|
||||
def test_seam_islands_remain_independent(self):
|
||||
p = (0, 0, 1, 0.5, 0.25, 0.25)
|
||||
left = interpolate_uv(p, [(0, 0), (.1, 0), (0, .1)])
|
||||
right = interpolate_uv(p, [(.9, .9), (1, .9), (.9, 1)])
|
||||
self.assertLess(left[0], .1)
|
||||
self.assertGreater(right[0], .9)
|
||||
|
||||
def test_hidden_rejected(self):
|
||||
tri = project_triangle([(-1, -1, 0, 1), (1, -1, 0, 1), (-1, 1, 0, 1)])
|
||||
self.assertEqual(list(visible_triangles(tri, lambda _: False, 1, 1)), [])
|
||||
|
||||
def test_mixed_visibility_conservative(self):
|
||||
tri = project_triangle([(-1, -1, 0, 1), (1, -1, 0, 1), (-1, 1, 0, 1)])
|
||||
accepted = list(visible_triangles(tri, lambda w: w[1] < .4, 1, 1))
|
||||
self.assertTrue(accepted)
|
||||
self.assertTrue(all(barycentric(p)[1] < .4 for t in accepted for p in t))
|
||||
self.assertLess(sum(area(list(t)) for t in accepted), .5)
|
||||
|
||||
def test_unresolved_depth_limit_rejected(self):
|
||||
tri = project_triangle([(-1, -1, 0, 1), (1, -1, 0, 1), (-1, 1, 0, 1)])
|
||||
self.assertEqual(list(visible_triangles(tri, lambda w: w[1] < .4, 1, 1, depth=10)), [])
|
||||
|
||||
def test_uv_tile_clips_without_repeating(self):
|
||||
poly = clip_rect([(-.2, .1), (.2, .1), (.2, .9)], 0, 1, 0, 1)
|
||||
self.assertTrue(all(0 <= p[0] <= .2 for p in poly))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user