update blender marker design
@@ -122,6 +122,66 @@ def fill_polygon_value(grid, width, height, polygon, value):
|
||||
x_end = max(0, min(width - 1, x_end))
|
||||
grid[y, x_start:x_end + 1] = value
|
||||
|
||||
def unwrap_periodic_values(values):
|
||||
"""
|
||||
Unwrap periodic UV coordinates so points straddling the 0/1 seam stay local.
|
||||
"""
|
||||
arr = np.asarray(values, dtype=np.float64).copy()
|
||||
if arr.size == 0:
|
||||
return arr
|
||||
if arr.max() - arr.min() > 0.5:
|
||||
arr[arr < 0.5] += 1.0
|
||||
return arr
|
||||
|
||||
def wrap_uv_polygon_for_raster(uv_poly):
|
||||
"""
|
||||
Return a seam-aware version of a UV polygon.
|
||||
|
||||
UV islands may cross the 0/1 seam. If we rasterize those raw coordinates as a
|
||||
single polygon, Blender's repeated UVs turn into a long diagonal triangle
|
||||
across the texture. We unwrap seam-crossing polygons first, then draw wrapped
|
||||
copies in the texture tile(s) they overlap.
|
||||
"""
|
||||
if len(uv_poly) < 3:
|
||||
return []
|
||||
|
||||
uv_arr = np.array([(float(u), float(v)) for u, v in uv_poly], dtype=np.float64)
|
||||
uv_arr[:, 0] = unwrap_periodic_values(uv_arr[:, 0])
|
||||
uv_arr[:, 1] = unwrap_periodic_values(uv_arr[:, 1])
|
||||
return [tuple(pt) for pt in uv_arr]
|
||||
|
||||
def uv_polygon_to_pixel_polygons(uv_poly, tex_width, tex_height):
|
||||
"""
|
||||
Convert one UV polygon into one or more pixel polygons, duplicating seam-
|
||||
crossing polygons into adjacent tiles so rasterization stays local.
|
||||
"""
|
||||
unwrapped = wrap_uv_polygon_for_raster(uv_poly)
|
||||
if len(unwrapped) < 3:
|
||||
return []
|
||||
|
||||
u_vals = [u for u, _ in unwrapped]
|
||||
v_vals = [v for _, v in unwrapped]
|
||||
u_shift_start = int(np.floor(min(u_vals)))
|
||||
u_shift_end = int(np.floor(max(u_vals)))
|
||||
v_shift_start = int(np.floor(min(v_vals)))
|
||||
v_shift_end = int(np.floor(max(v_vals)))
|
||||
|
||||
pixel_polys = []
|
||||
for u_shift in range(u_shift_start, u_shift_end + 1):
|
||||
for v_shift in range(v_shift_start, v_shift_end + 1):
|
||||
shifted = [(u - u_shift, v - v_shift) for u, v in unwrapped]
|
||||
shifted_u = [u for u, _ in shifted]
|
||||
shifted_v = [v for _, v in shifted]
|
||||
if max(shifted_u) < 0.0 or min(shifted_u) > 1.0:
|
||||
continue
|
||||
if max(shifted_v) < 0.0 or min(shifted_v) > 1.0:
|
||||
continue
|
||||
pixel_polys.append([
|
||||
(u * (tex_width - 1), v * (tex_height - 1))
|
||||
for u, v in shifted
|
||||
])
|
||||
return pixel_polys
|
||||
|
||||
def draw_ellipse(pixels, width, height, cx, cy, a, b, angle, color):
|
||||
"""Filled ellipse with semi‑axes a,b (pixels), rotation angle (rad)."""
|
||||
cos_a = np.cos(angle)
|
||||
@@ -194,7 +254,8 @@ def raycast_uv_from_camera(scene, depsgraph, cam, obj, u_ndc, v_ndc, frame_width
|
||||
|
||||
def convex_hull_uv(points):
|
||||
"""Return UV points ordered around their 2D convex hull."""
|
||||
unique = sorted({(float(p.x), float(p.y)) for p in points})
|
||||
unwrapped = wrap_uv_polygon_for_raster([(p.x, p.y) for p in points])
|
||||
unique = sorted(set(unwrapped))
|
||||
if len(unique) < 3:
|
||||
return None
|
||||
|
||||
@@ -282,8 +343,8 @@ def rasterize_mesh_uvs(obj, pixels, tex_width, tex_height, color=(1,1,1,1)):
|
||||
uv = uv_data[loop_idx].uv
|
||||
uv_poly.append((uv.x, uv.y))
|
||||
# Convert UV (0‑1) to pixel coordinates
|
||||
pixel_poly = [(u * (tex_width - 1), v * (tex_height - 1)) for u, v in uv_poly]
|
||||
fill_polygon(None, pixels, tex_width, tex_height, pixel_poly, color)
|
||||
for pixel_poly in uv_polygon_to_pixel_polygons(uv_poly, tex_width, tex_height):
|
||||
fill_polygon(None, pixels, tex_width, tex_height, pixel_poly, color)
|
||||
|
||||
def rasterize_mesh_mask(obj, tex_width, tex_height):
|
||||
"""Rasterize the active UV map to a boolean mesh mask."""
|
||||
@@ -298,8 +359,8 @@ def rasterize_mesh_mask(obj, tex_width, tex_height):
|
||||
for loop_idx in poly.loop_indices:
|
||||
uv = uv_data[loop_idx].uv
|
||||
uv_poly.append((uv.x, uv.y))
|
||||
pixel_poly = [(u * (tex_width - 1), v * (tex_height - 1)) for u, v in uv_poly]
|
||||
fill_polygon_value(mask, tex_width, tex_height, pixel_poly, True)
|
||||
for pixel_poly in uv_polygon_to_pixel_polygons(uv_poly, tex_width, tex_height):
|
||||
fill_polygon_value(mask, tex_width, tex_height, pixel_poly, True)
|
||||
return mask
|
||||
|
||||
def paint_mask_color(pixels, width, height, mask, color):
|
||||
@@ -495,10 +556,10 @@ def generate_patch_texture(
|
||||
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)
|
||||
for pixel_poly in uv_polygon_to_pixel_polygons(uv_poly, texture_width, texture_height):
|
||||
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||
|
||||
# (after drawing all patches)
|
||||
if uv_centre is not None:
|
||||
@@ -583,17 +644,18 @@ def generate_checker_texture(
|
||||
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)
|
||||
fill_polygon_value(
|
||||
patch_state,
|
||||
texture_width,
|
||||
texture_height,
|
||||
pixel_poly,
|
||||
1 if np.mean(color) >= 0.5 else 0,
|
||||
)
|
||||
patch_value = 1 if np.mean(color) >= 0.5 else 0
|
||||
for pixel_poly in uv_polygon_to_pixel_polygons(uv_poly, texture_width, texture_height):
|
||||
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||
fill_polygon_value(
|
||||
patch_state,
|
||||
texture_width,
|
||||
texture_height,
|
||||
pixel_poly,
|
||||
patch_value,
|
||||
)
|
||||
|
||||
complete_checker_gaps(
|
||||
pixels, texture_width, texture_height, mesh_mask, patch_state, max_gap_px=gap_fill_px
|
||||
@@ -748,10 +810,10 @@ def generate_dot_texture(
|
||||
def main():
|
||||
# ==================== CONFIGURATION ====================
|
||||
MODE = "patches" # "patches", "checker", or "dots"
|
||||
OBJ_NAME = "FingerMesh"
|
||||
CAM_NAME = "Camera"
|
||||
TEXTURE_WIDTH = 4096
|
||||
TEXTURE_HEIGHT = 4096
|
||||
OBJ_NAME = "FingerMesh0525.001"
|
||||
CAM_NAME = "Camera0607"
|
||||
TEXTURE_WIDTH = 1640*2
|
||||
TEXTURE_HEIGHT = 1600*2
|
||||
OUTPUT_NAMES = {
|
||||
"patches": "patch_texture.png",
|
||||
"checker": "checker_texture.png",
|
||||
@@ -760,7 +822,7 @@ def main():
|
||||
OUTPUT_PATH = "//" + OUTPUT_NAMES.get(MODE, "texture.png")
|
||||
|
||||
# Patch mode
|
||||
PATCH_NX = 60; PATCH_NY = 50; COLOR_R = 0.05
|
||||
PATCH_NX = 80; PATCH_NY = 45; COLOR_R = 0.05
|
||||
EDGE_SAMPLE_GRID = 5
|
||||
# Checker mode
|
||||
CHECKER_NX = 40; CHECKER_NY = 40; CHECKER_GAP_FILL_PX = 6
|
||||
|
||||
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 795 KiB |
|
Before Width: | Height: | Size: 898 KiB |
|
Before Width: | Height: | Size: 1002 KiB |