"""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 = {} # ``cache`` and ``world`` are bound as defaults: ``cache`` is rebound # for every loop triangle, and a late-bound closure would read the # next triangle's cache (or the final triangle's, if a probe were # deferred), yielding a wrong visibility answer per triangle. def visible(weights, cache=cache, world=world): 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('Visibility sampling: seven probes per fragment; uniformly probed ' 'fragments are accepted or dropped whole, mixed boundary fragments ' 'refine to 1/4 cell or depth 6 and unresolved leaves are rejected. ' 'Sub-sample occluders may be missed; hit tolerance max(1e-7, distance*2e-5).') return output finally: evaluated.to_mesh_clear()