Pixel-Unwrapper icon indicating copy to clipboard operation
Pixel-Unwrapper copied to clipboard

Just found your addon and I've got a few things.

Open BrahRah opened this issue 1 year ago • 11 comments

This is how I handle trapezoids: Screenshot 2024-02-13 18:35:08

Note the aligning pixels on the edge: Screenshot 2024-02-13 18:35:45

you can even do: Screenshot 2024-02-13 18:37:49

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: Screenshot 2024-02-13 18:49:57

What creates the most work for me is blenders unwrap: Screenshot 2024-02-13 18:57:43 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: Screenshot 2024-02-13 18:59:52

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.

BrahRah avatar Feb 13 '24 17:02 BrahRah

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.

noio avatar Feb 15 '24 08:02 noio

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.

BrahRah avatar Feb 16 '24 11:02 BrahRah

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:

Screenshot 2024-02-16 at 14 24 40

noio avatar Feb 16 '24 13:02 noio

For me it look like this if I do it: Screenshot 2024-02-16 14:27:47

With the right scale and placement of the faces I think unwrap basic would work.

BrahRah avatar Feb 16 '24 13:02 BrahRah

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?)

noio avatar Feb 16 '24 13:02 noio

It's the standard blender cube with 111 bu where I resize the front face to a trapezoid: Screenshot 2024-02-16 14:37:31

Screenshot 2024-02-16 14:37:55 pixels per unit is the same size as the texture

BrahRah avatar Feb 16 '24 13:02 BrahRah

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.

noio avatar Feb 16 '24 13:02 noio

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: Screenshot 2024-02-16 14:46:33

It's slightly off

I redid it with a new cube and the issue did not reappear.

BrahRah avatar Feb 16 '24 13:02 BrahRah

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.

BrahRah avatar Feb 16 '24 14:02 BrahRah

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.

BrahRah avatar Feb 16 '24 18:02 BrahRah

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: Screenshot 2024-02-17 14:02:35

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?

BrahRah avatar Feb 17 '24 11:02 BrahRah