Just found your addon and I've got a few things.
This is how I handle trapezoids:
Note the aligning pixels on the edge:
you can even do:
I'd really like to use your addon but you use the tilemap/spritesheet workflow where I use the repeating texture workflow. aka I use a 16x16px texture per material and apply that to a surface and make it tile by sizing the uv larger then the texture size. (as you can see in the uv edit view)
Would it be possible to add a feature that places all islands on top of each other on the texture? Like this:
What creates the most work for me is blenders unwrap:
As the projection/size and rotation is completely off.
I solve that by aligning the one edge of a none square face to the textures length:
This works fairly well, if there is no edge that is the size of the texture this gets a bit fiddly tho.
The most pixel perfect results I get is to start with a cube and then place the islands. After that then use correct face attributes to reshape the cube into non square faces. If not done like this it becomes a mess and nothing aligns. However this is not that easy to model with.
Maybe some of this process can be applied to your addon.
Hey @BrahRah ! Love learning about other Pixel 3D workflows.
You are right that the paradigm of Pixel Unwrapper is quite a bit different from the method you're describing.
Follow Edges vs Square Pixels Pixel Unwrapper's Unwrap Grid kind of makes an opposite assumption! That if you have a quad face, it should be mapped to a rectangular UV section (deforming the pixels). That's because I personally usually prioritize pixel edges following model edges.
If I understand correctly, you prioritize having square pixels, even on trapezoids, and you want want quads (trapezoids) to actually retain square pixels, and the mapping should follow one edge? I can see the use of that!
The question there is UI related. How do you indicate to blender which edge you want to use as the guide? And what is the behavior when multiple faces are selected?
Atlas vs Repeating Texture Pixel Unwrapper also fairly heavily leans in to having a texture that's uniquely mapped to bits of the model, because I designed it to help with a workflow that leads to direct texture painting. That's why many operators will immediately move the newly mapped UV island to a bit of free space on the UV map. I could consider just making that 2 actions though (1: map UV, 2: move to free space) instead of doing both with a single click.
I think in most cases the smallest edge is going to be the one that needs to align with the texture/img size. But for this to work in an addon I assume the best way would be to select the edge and then use a button to mark it as the edge to be aligned with the texture.
For multiple faces as long as they have one line of edges that can be placed on the texture border I assume that the selection/marking action should work too.
The hardest part if even possible in blender would be to detect the texture image borders I assume. Especially the zoom will make that messy. But maybe the zoom can be set to some standard level first for the script.
I've been thinking we have the texel size. Aka pixel to blender unit ratio. So if the edge length is known then wouldn't it be possible to use that to align the uv? As long as the 3d viewport edge length and uv edge length are the same it could be possible to auto size the mesh to the pixels.
I did some testing with chatgpt: blender uv test.blend.zip (blend file with script)
Script:
import bpy
import bmesh
import mathutils
def blender_unit_size_of_selected_edge():
# Check if there's an active UV map
if not bpy.context.active_object.data.uv_layers:
print("No active UV map.")
return None
# Get the active UV map
uv_layer = bpy.context.active_object.data.uv_layers.active.data
# Get the selected edge
selected_edge = None
for edge in bpy.context.active_object.data.edges:
if edge.select:
selected_edge = edge
break
if not selected_edge:
print("No edge selected.")
return None
# Get the UV coordinates of the edge's vertices
v1_uv = uv_layer[selected_edge.vertices[0]].uv
v2_uv = uv_layer[selected_edge.vertices[1]].uv
# Calculate the distance between the UV coordinates of the two vertices
distance = (v2_uv - v1_uv).length
return distance
# Test the function
blender_unit_size = blender_unit_size_of_selected_edge()
if blender_unit_size is not None:
print("Blender unit size of selected edge:", blender_unit_size)
I'm not a blender pro but an 1bu edge is 1.4142135623730951bu in uv edit mode. This seems to be constant and is independent from the zoom level. Maybe this can be used to automate the resizing via the set texel density.
Are you sure about the 'shortest edge' being the one to follow? If you use blender's standard Unwrap on a single face, it will use the longest edge and that seems like a good choice.
If you use Pixel Unwrapper's "Unwrap Basic" I think that almost does what you're looking for? It will keep the pixels square and also scale to the correct Texel Density:
For me it look like this if I do it:
With the right scale and placement of the faces I think unwrap basic would work.
Did you set the correct "Pixels per Unit" on Pixel Unwrapper's panel? And is your object not scaled itself? (I.e. did you apply the scale?)
It's the standard blender cube with 111 bu where I resize the front face to a trapezoid:
pixels per unit is the same size as the texture
Yeah. I don't think your cube is "111bu". A standard blender cube is 2 x 2 x 2 units in size. (See Dimensions: on your screenshot).
With 16 pixels per unit that means each face will get 2 x 16 = 32 pixels to a side. Which is what I'm seeing in your image. So that all checks out.
Either scale down your cube or set the Pixels Per Unit to 8, if you want your 16px texture to span the whole cube.
I redid it with a 111 dim (scale also at 111) cube but it seems the unwrap basic has some issues with that small size:
It's slightly off
I redid it with a new cube and the issue did not reappear.
This is actually a pretty good workflow already:
https://github.com/noio/Pixel-Unwrapper/assets/40721994/589c502b-9aea-4247-8c7e-71d36aedadc4
Just need to remember to start with a 111 dim cube.
I'll check if it's possible to automate the uv placements over the imgs borders next.
I think I found a way to get the image borders:
bpy.ops.image.view_all(fit_view=True) if that is the correct one
and:
def get_uv_editor_area_size():
for area in bpy.context.screen.areas:
if area.type == 'UV_EDITOR':
width = area.width
height = area.height
print("UV Editor Area Size:", width, "x", height)
break
# Call the function to get the UV editor area size
get_uv_editor_area_size()
and:
import bpy
def get_materials_images_size():
# Check if any objects are selected
if not bpy.context.selected_objects:
print("No objects selected.")
return
# Iterate over selected objects
for obj in bpy.context.selected_objects:
print("Object:", obj.name)
# Iterate over materials of the object
for slot in obj.material_slots:
material = slot.material
if material:
print("Material:", material.name)
# Iterate over material nodes
for node in material.node_tree.nodes:
if node.type == 'TEX_IMAGE':
image = node.image
if image:
print("Image:", image.name)
print("Size:", image.size[:])
print() # Add an empty line for readability
# Call the function to get materials' image sizes
get_materials_images_size()
or maybe just fractional zoom 1:1 + image size as that seems to be the same size independent of area size.
if bpy.ops.image also works in the uv edit tab and not just bpy.ops.uv there might be better options sadly I can't test it as I'm struggling switching to the uv editor tab via script.
just found out that I can't switch context via the scripting tab scripts and that bpy.ops.image should work in the UV_EDITOR context. Gonna test that tomorrow tho it's gotten late here.
bpy.ops.image doesn't seem to have anything useful zooming and centering the image. So my mentioned method is still prob. the only way to get the images bound on the canvas.
I think the concept works:
import bpy
bl_info = {
"name" : "uv test",
"author" : "Brah Rah",
"description" : "",
"blender" : (3, 0, 0),
"version" : (0, 0, 1),
"location" : "",
"warning" : "",
"doc_url": "",
"tracker_url": "",
"category" : "Image Editor"
}
def get_uv_editor_area_size():
for area in bpy.context.screen.areas:
if area.type == 'IMAGE_EDITOR':
width = area.width
height = area.height
#print("UV Editor Area Size:", width, "x", height)
return width, height
print("UV Editor Area not found.")
return None
def get_border_points_on_image_editor_canvas():
image_size = get_image_size_in_editor()
uv_editor_size = get_uv_editor_area_size()
if image_size and uv_editor_size:
img_width, img_height = image_size
editor_width, editor_height = uv_editor_size
# Calculate the center of the UV editor canvas
center_x = editor_width / 2
center_y = editor_height / 2
# Calculate the half-width and half-height of the image
half_img_width = img_width / 2
half_img_height = img_height / 2
# Calculate the coordinates of the image corners relative to the UV editor canvas
pointUpLeft = (center_x - half_img_width, center_y + half_img_height)
pointUpRight = (center_x + half_img_width, center_y + half_img_height)
pointDownLeft = (center_x - half_img_width, center_y - half_img_height)
pointDownRight = (center_x + half_img_width, center_y - half_img_height)
return pointUpLeft, pointUpRight, pointDownLeft, pointDownRight
else:
print("Failed to get image size or UV Editor area size.")
return None
#doesn't work
def annotate_point(x, y):
# Set the cursor location to the specified coordinates
bpy.context.scene.cursor.location = (x, y, 0)
# Activate the built-in annotate tool
bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
# Simulate mouse click and release to draw the point
#bpy.ops.annotation.draw("INVOKE_DEFAULT", mode='DRAW', stroke='POINT')
def normalize_coordinates(point, uv_editor_size):
"""
Normalize coordinates to the range [0, 1].
Args:
point (tuple): (x, y) coordinates.
uv_editor_size (tuple): (width, height) of the UV editor area.
Returns:
tuple: Normalized coordinates in the range [0, 1].
"""
width, height = uv_editor_size
x_normalized = point[0] / width
y_normalized = point[1] / height
return x_normalized, y_normalized
def get_image_size_in_editor():
# Get the active space
space = bpy.context.space_data
# Check if the active space is an Image Editor
if space.type == 'IMAGE_EDITOR':
# Check if an image is loaded
if space.image:
# Get the dimensions of the loaded image
width, height = space.image.size[:]
return width, height
else:
print("No image loaded in the Image Editor.")
return None
else:
print("The active space is not an Image Editor.")
return None
class Button1OP(bpy.types.Operator):
"""Does stuff"""
bl_idname = "uv.button1"
bl_label = "Button1"
def execute(self, context):
bpy.ops.image.view_zoom_ratio(ratio=1)
get_uv_editor_area_size()
get_image_size_in_editor()
border_points = get_border_points_on_image_editor_canvas()
if border_points:
print("Border Points on UV Editor Canvas:")
print("Point Up Left:", border_points[0])
print("Point Up Right:", border_points[1])
print("Point Down Left:", border_points[2])
print("Point Down Right:", border_points[3])
pointUpLeft, pointUpRight, pointDownLeft, pointDownRight = border_points
#annotate_point(pointUpLeft[0], pointUpLeft[1])
#annotate_point(pointUpRight[0], pointUpRight[1])
#annotate_point(pointDownLeft[0], pointDownLeft[1])
#annotate_point(pointDownRight[0], pointDownRight[1])
uv_editor_size = get_uv_editor_area_size()
if uv_editor_size:
normalized_points = [normalize_coordinates(point, uv_editor_size) for point in border_points]
print("Border Points Normalized:")
print("Point Up Left:", normalized_points[0])
print("Point Up Right:", normalized_points[1])
print("Point Down Left:", normalized_points[2])
print("Point Down Right:", normalized_points[3])
x, y = normalized_points[0]
move_selected_uv_to(x, y)
# Example usage:
image_size = get_image_size_in_editor()
if image_size:
print("Image size:", image_size)
return {'FINISHED'}
class UVPanel(bpy.types.Panel):
"""Creates a Panel in the UV Editor"""
bl_label = "UV Tools"
bl_idname = "UV_PT_Panel"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = 'UV'
def draw(self, context):
layout = self.layout
row = layout.row()
row.operator("uv.button1")
def register():
bpy.utils.register_class(Button1OP)
bpy.utils.register_class(UVPanel)
def unregister():
bpy.utils.unregister_class(Button1OP)
bpy.utils.unregister_class(UVPanel)
if __name__ == "__main__":
register()
I have not been able to mark/annotate the corners to see if it works tho. That part is harder then the rest of the script....
but I checked the coordinates it creates and the image position they seem correct:
the bpy.ops.image.view_zoom_ratio(ratio=1) size on the screen/uv edit canvas is the same size as the image 16x16.
Would you be able to place the uv islands on top of the image with this?