278 lines
9.1 KiB
Python
278 lines
9.1 KiB
Python
import bpy
|
|
import bmesh
|
|
import numpy as np
|
|
from mathutils import Vector
|
|
import os
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Scanline polygon fill for Blender Image pixels
|
|
# ----------------------------------------------------------------------
|
|
def fill_polygon(image, pixels, width, height, polygon, color):
|
|
poly = [(int(round(x)), int(round(y))) for x, y in polygon]
|
|
if len(poly) < 3:
|
|
return
|
|
|
|
min_x = max(0, min(p[0] for p in poly))
|
|
max_x = min(width-1, max(p[0] for p in poly))
|
|
min_y = max(0, min(p[1] for p in poly))
|
|
max_y = min(height-1, max(p[1] for p in poly))
|
|
|
|
for y in range(min_y, max_y + 1):
|
|
intersections = []
|
|
for i in range(len(poly)):
|
|
x1, y1 = poly[i]
|
|
x2, y2 = poly[(i+1) % len(poly)]
|
|
if y1 == y2:
|
|
continue
|
|
if (y1 <= y < y2) or (y2 <= y < y1):
|
|
t = (y - y1) / (y2 - y1)
|
|
x = x1 + t * (x2 - x1)
|
|
intersections.append(x)
|
|
intersections.sort()
|
|
for k in range(0, len(intersections), 2):
|
|
if k+1 >= len(intersections):
|
|
break
|
|
x_start = int(round(intersections[k]))
|
|
x_end = int(round(intersections[k+1]))
|
|
x_start = max(0, min(width-1, x_start))
|
|
x_end = max(0, min(width-1, x_end))
|
|
for x in range(x_start, x_end + 1):
|
|
idx = (y * width + x) * 4
|
|
pixels[idx:idx+4] = color
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Get interpolated UV from face and hit location (fixed version)
|
|
# ----------------------------------------------------------------------
|
|
def get_uv_from_face(obj, face_index, location):
|
|
me = obj.data
|
|
local_loc = obj.matrix_world.inverted() @ location
|
|
|
|
poly = me.polygons[face_index]
|
|
vert_indices = poly.vertices
|
|
loop_indices = poly.loop_indices
|
|
|
|
if len(vert_indices) < 3:
|
|
return None
|
|
|
|
v0 = me.vertices[vert_indices[0]].co
|
|
v1 = me.vertices[vert_indices[1]].co
|
|
v2 = me.vertices[vert_indices[2]].co
|
|
|
|
uv_layer = me.uv_layers.active.data
|
|
uv0 = uv_layer[loop_indices[0]].uv
|
|
uv1 = uv_layer[loop_indices[1]].uv
|
|
uv2 = uv_layer[loop_indices[2]].uv
|
|
|
|
def tri_area(a, b, c):
|
|
return 0.5 * ((b - a).cross(c - a)).length
|
|
|
|
total_area = tri_area(v0, v1, v2)
|
|
if total_area == 0:
|
|
return None
|
|
|
|
w0 = tri_area(local_loc, v1, v2) / total_area
|
|
w1 = tri_area(local_loc, v2, v0) / total_area
|
|
w2 = 1.0 - w0 - w1
|
|
|
|
uv = w0 * uv0 + w1 * uv1 + w2 * uv2
|
|
return Vector((uv.x, uv.y))
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Color generation functions (unchanged)
|
|
# ----------------------------------------------------------------------
|
|
def merge_intervals(intervals):
|
|
if not intervals:
|
|
return []
|
|
intervals.sort(key=lambda x: x[0])
|
|
merged = [intervals[0]]
|
|
for current in intervals[1:]:
|
|
last = merged[-1]
|
|
if current[0] <= last[1]:
|
|
merged[-1] = (last[0], max(last[1], current[1]))
|
|
else:
|
|
merged.append(current)
|
|
return merged
|
|
|
|
def get_allowed_intervals(neighbor_values, delta):
|
|
forbidden = []
|
|
for c in neighbor_values:
|
|
low = max(0, c - delta)
|
|
high = min(1, c + delta)
|
|
forbidden.append((low, high))
|
|
merged = merge_intervals(forbidden)
|
|
allowed = []
|
|
if merged:
|
|
if merged[0][0] > 0:
|
|
allowed.append((0, merged[0][0]))
|
|
for i in range(1, len(merged)):
|
|
allowed.append((merged[i-1][1], merged[i][0]))
|
|
if merged[-1][1] < 1:
|
|
allowed.append((merged[-1][1], 1))
|
|
else:
|
|
allowed.append((0, 1))
|
|
return allowed
|
|
|
|
def sample_from_allowed(allowed):
|
|
lengths = [end - start for start, end in allowed]
|
|
total_length = sum(lengths)
|
|
if total_length <= 0:
|
|
return None
|
|
point = np.random.rand() * total_length
|
|
cumulative = 0
|
|
for (start, end), length in zip(allowed, lengths):
|
|
cumulative += length
|
|
if point <= cumulative:
|
|
return np.random.uniform(start, end)
|
|
return allowed[-1][0]
|
|
|
|
def sample_channel(neighbor_values, delta):
|
|
allowed = get_allowed_intervals(neighbor_values, delta)
|
|
if not allowed:
|
|
return None
|
|
return sample_from_allowed(allowed)
|
|
|
|
def generate_valid_color(neighbors, r):
|
|
delta = np.sqrt(r)
|
|
color = np.zeros(3)
|
|
for channel in range(3):
|
|
neighbor_channel = [n[channel] for n in neighbors]
|
|
sampled = sample_channel(neighbor_channel, delta)
|
|
if sampled is None:
|
|
sampled = np.random.rand()
|
|
color[channel] = sampled
|
|
return color
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Main function
|
|
# ----------------------------------------------------------------------
|
|
def generate_fingerprint_texture(
|
|
obj_name=None,
|
|
camera_name="Camera",
|
|
nx=40,
|
|
ny=30,
|
|
r=0.05,
|
|
texture_width=2048,
|
|
texture_height=2048,
|
|
output_path="//finger_texture.png"
|
|
):
|
|
scene = bpy.context.scene
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
|
|
if obj_name:
|
|
obj = bpy.data.objects[obj_name]
|
|
else:
|
|
obj = bpy.context.active_object
|
|
if obj is None or obj.type != 'MESH':
|
|
raise ValueError("No valid mesh object selected")
|
|
|
|
cam = bpy.data.objects[camera_name]
|
|
if cam is None or cam.type != 'CAMERA':
|
|
raise ValueError("Camera not found")
|
|
|
|
if not obj.data.uv_layers.active:
|
|
obj.data.uv_layers.new(name="UVMap")
|
|
|
|
render = scene.render
|
|
res_x = render.resolution_x
|
|
res_y = render.resolution_y
|
|
|
|
cam_data = cam.data
|
|
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
|
|
|
|
patch_uv_polygons = []
|
|
patch_colors = np.zeros((ny, nx, 3), dtype=np.float32)
|
|
|
|
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
|
|
|
|
corners_ndc = [
|
|
(u_start, v_start),
|
|
(u_end, v_start),
|
|
(u_end, v_end),
|
|
(u_start, v_end)
|
|
]
|
|
|
|
uv_poly = []
|
|
for (u_ndc, v_ndc) in corners_ndc:
|
|
sx = (u_ndc - 0.5) * frame_width
|
|
sy = (v_ndc - 0.5) * frame_height
|
|
direction_cam = Vector((sx, sy, -1.0))
|
|
direction_world = cam.matrix_world.to_quaternion() @ direction_cam
|
|
direction_world.normalize()
|
|
origin = cam.matrix_world.translation
|
|
|
|
hit, location, normal, face_index, hit_obj, matrix = scene.ray_cast(depsgraph, origin, direction_world)
|
|
if hit and hit_obj == obj:
|
|
uv = get_uv_from_face(obj, face_index, location)
|
|
if uv is not None:
|
|
uv_poly.append(uv)
|
|
|
|
if len(uv_poly) < 3:
|
|
patch_uv_polygons.append(None)
|
|
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])
|
|
|
|
if len(neighbors) == 0:
|
|
color = np.random.rand(3)
|
|
else:
|
|
color = generate_valid_color(neighbors, r)
|
|
patch_colors[j, i] = color
|
|
|
|
img_name = "FingerprintTexture"
|
|
if img_name in bpy.data.images:
|
|
bpy.data.images.remove(bpy.data.images[img_name])
|
|
image = bpy.data.images.new(img_name, width=texture_width, height=texture_height, alpha=True)
|
|
pixels = list(image.pixels)
|
|
|
|
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
|
|
pixel_poly = [(u * (texture_width - 1), v * (texture_height - 1)) for (u, v) in uv_poly]
|
|
color = patch_colors[j, i]
|
|
rgba = (color[0], color[1], color[2], 1.0)
|
|
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
|
|
|
image.pixels[:] = pixels
|
|
image.update()
|
|
|
|
abs_path = bpy.path.abspath(output_path)
|
|
image.filepath_raw = abs_path
|
|
image.file_format = 'PNG'
|
|
image.save()
|
|
print(f"Texture saved to {abs_path}")
|
|
return image
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Run
|
|
# ----------------------------------------------------------------------
|
|
if __name__ == "__main__":
|
|
generate_fingerprint_texture(
|
|
obj_name="FingerMesh", # Change to your mesh name
|
|
nx=50, ny=40,
|
|
r=0.05,
|
|
texture_width=4096,
|
|
texture_height=4096,
|
|
output_path="//finger_texture.png"
|
|
) |