78 lines
4.3 KiB
Python
78 lines
4.3 KiB
Python
"""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()
|