2
$\begingroup$

I'm working with a mesh created for a rendering engine that has no problem with UVs folded upon themselves, when using a normal map. The destination engine certainly does however, so it's important that I unflip these UVs.

Through my own research, I've discovered that Blender will display the mesh as it would appear in the other engine, as far as flipped UVs go, by leaving the normal map's color space set to sRBG rather than non-color. Yes, I realize you're supposed to set it to non-color, and by doing so the mesh looks great, but my goal isn't to just use it in Blender.

So, the fix, in theory, is pretty straightforward—flip the UV face in the proper direction in relation to the tile it exists in, and then change its material with textures that are flipped in the same direction. This way, the UVs are still mapped to the exact same area of the texture after they're no longer flipped. Of course, the appropriate channel of the normal map is inverted (red for horizontal flipping, green for vertical).

Unfortunately, other than looking at the UVs to see how they're folded, I have no way to determine direction. Even worse, the way Blender identifies flipped UVs is a bit too simplistic. Namely comparing the sign of the area of the UV triangle and its corresponding mesh triangle. Now this will detect flipped UVs, but only if they're flipped in one direction. This is because if you flip a UV in both directions, it's effectively just rotated 180 degrees, rather than inverted.

So, what I need to do is go over every UV triangle and determine its direction relative to its corresponding mesh triangle. This way, I should be able to figure out which way it needs to be flipped and make the appropriate changes.

Only problem is that I'm a bit out of my league on the necessary math. I'm thinking comparing the direction of each UV triangle edge to its corresponding mesh edge, should give me an idea?

upon feedback, I've replaced the original blender file with one of the actual project meshes, and to start out, here's the normal map:

The default normal map

and here's a rundown of what the issue is: This is the default mesh, with its normal map applied, set to the color space of sRGB, and material settings tweaked to eliminate all shine/reflection. Note the splitting and abrupt changes in shading. This is an indication of flipped UVs

With Magic UV enabled, I can select flipped UVs, which correlates perfectly with the previous screenshot. The reason everything looks selected on the left is because the UV mapping which is often folded in upon itself in order to conserve space.

Unfortunately, fixing this isn't quite as simple as flipping these UV faces in place, as these faces all must retain their position in relation to the texture in order for the mesh to display the intended detail. The solution is in 3 parts.

  1. separate the flipped UV faces from a particular UV island into a separate mesh

  2. flip these faces in the correct direction in relation to the UV tile it's on. So if the original flipped UV faces are on the left side of the tile, and you flip them horizontally, they should end up on the right side.

  3. create a new set of textures by horizontally flipping the originals - being sure to invert the red channel of the Normal map to make said map work as intended. Do the equivalent for a third set of textures, this time vertically flipping them.

  4. apply the horizontally flipped textures to the new mesh.

  5. repeat until everything is flipped.

After applying this method to the outer ring, you can tell the splitting is gone, and the shading is now consistent. This is the goal, so lets move onto the inner ring.

And now we have a problem - yes, it's true that the seams are gone, but now we're left with a smooth gradient from one end to the other.

$\endgroup$
10
  • 2
    $\begingroup$ Could you provide a Blender file with "well defined UVs" for a representative object and "flipped UVs" for a duplicate of this same object (along U, along V, and along both U and V) ? I think a GeometryNodes modifier could do what you are looking for. You can use blend-exchange.com to share this minimal setup. $\endgroup$ Commented Jun 2 at 8:33
  • $\begingroup$ Honestly, I don't understand the question. Illustrating your question with some sketches, or screenshots of the .blend and arrows pointing to what exactly and how it is flipped could be a good way of communicating the problem further. Also consider using a simpler mesh with fewer faces as an example so it's easier to localize the changes and reason about the logic behind the changes... $\endgroup$ Commented Jun 4 at 7:42
  • 1
    $\begingroup$ pls add the images + text to your question, not as external link in the comments... $\endgroup$
    – Chris
    Commented Jun 4 at 10:38
  • 1
    $\begingroup$ Could you confirm that in the provided Blender file, the "Sphere" object has "well defined UVs", while the "Sphere.001" object is the pathological one ? $\endgroup$ Commented Jun 4 at 20:51
  • 1
    $\begingroup$ Thanks. I am still working on this. Quite a challenge ! $\endgroup$ Commented Jun 13 at 5:57

4 Answers 4

3
+50
$\begingroup$

The mirrored normals is a unique data and either must be resolved at the renderer level or baked into a new normal map.

Duplicate and flip

A script to flip mirrored UVs along side with creating a second material and a second set of mirrored textures. This will not work if there are procedural UV transformations.

enter image description here

import typing
import bpy
import os


T = typing.TypeVar('T')
T2 = typing.TypeVar('T2')


def list_by_key(items: typing.Collection[T], key_func: typing.Callable[[T], T2]) -> typing.Dict[T2, typing.List[T]]:
    result: typing.Dict[T2, typing.List[T]] = {}
    for item in items:
        result.setdefault(key_func(item), []).append(item)
    return result


def select_objects(objects):

    import bpy

    object: bpy.types.Object
    for object in bpy.context.view_layer.objects:
        object.select_set(object in objects)

    bpy.context.view_layer.objects.active = objects[0]

    return objects


def flip_image(image, flip_x_channel = False):

    import numpy

    pixels = numpy.empty(len(image.pixels), dtype = numpy.float32)
    image.pixels.foreach_get(pixels)

    x, y = image.size
    pixels = pixels.reshape((x, y, 4))

    pixels = numpy.fliplr(pixels)

    if flip_x_channel:
        pixels[:, :, 0] = 1 - pixels[:, :, 0]

    name, ext = os.path.splitext(image.name)
    new_name = name + '_flipped' + ext

    new_image = bpy.data.images.new(name = new_name, width = x, height = y, is_data = image.colorspace_settings.is_data)
    new_image.colorspace_settings.name = image.colorspace_settings.name
    new_image.pixels.foreach_set(pixels.flatten())

    return new_image


def flip_images(material):

    image_nodes = [node for node in material.node_tree.nodes if node.bl_idname == 'ShaderNodeTexImage' and node.image]

    unique_images = list_by_key(image_nodes, lambda x: x.image)

    normal_image_nodes = set()

    for link in material.node_tree.links:
        if link.to_node.bl_idname == 'ShaderNodeNormalMap' and link.from_node.bl_idname == 'ShaderNodeTexImage':
            normal_image_nodes.add(link.from_node)

    for image, nodes in unique_images.items():

        if not image.pixels:
            continue

        flipped_image = flip_image(image, flip_x_channel = not normal_image_nodes.isdisjoint(nodes))

        for node in nodes:
            node.image = flipped_image



def flip_mirrored_uvs_and_images(objects):

    import bpy
    import bmesh

    bpy.ops.preferences.addon_enable(module='magic_uv')
    bpy.context.scene.tool_settings.use_uv_select_sync = False

    mesh_to_objects = list_by_key(objects, lambda x: x.data)

    mesh: bpy.types.Mesh
    for mesh, objects in mesh_to_objects.items():

        select_objects([objects[0]])


        # material and image
        flipped_material = mesh.materials[0].copy()
        flipped_material.name = mesh.materials[0].name + '_flipped'
        mesh.materials.append(flipped_material)
        material_index = len(mesh.materials) - 1

        flip_images(flipped_material)

        # uv and material_index
        init_mode = objects[0].mode
        bpy.ops.object.mode_set(mode='EDIT', toggle=False)

        bpy.ops.mesh.reveal()

        b_mesh = bmesh.from_edit_mesh(mesh)

        for uv_layer in mesh.uv_layers:
            uv_layer.active = True

            bpy.ops.uv.reveal()

            bpy.ops.uv.muv_select_uv_select_flipped(selection_method='RESET', sync_mesh_selection=False)

            uv_layer = b_mesh.loops.layers.uv.verify()

            for face in b_mesh.faces:
                for loop in face.loops:
                    if loop[uv_layer].select:
                        face.material_index = material_index
                        loop[uv_layer].uv[0] = 1 - loop[uv_layer].uv[0]

        bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False)

        bpy.ops.object.mode_set(mode=init_mode, toggle=False)


flip_mirrored_uvs_and_images([bpy.context.object])

Baking

A script to separate the flipped UVs to later pack the UVs with preserving overlaps and bake the normals but that's no longer a trimsheet and repeats the mirrored normals.

There could be a better solution that preserves the trimsheet quality of the texture and does not use bpy.ops.uv.muv_clip_uv.

enter image description here

enter image description here

import typing

T = typing.TypeVar('T')
T2 = typing.TypeVar('T2')


def list_by_key(items: typing.Collection[T], key_func: typing.Callable[[T], T2]) -> typing.Dict[T2, typing.List[T]]:
    result: typing.Dict[T2, typing.List[T]] = {}
    for item in items:
        result.setdefault(key_func(item), []).append(item)
    return result


def select_objects(objects):

    import bpy

    object: bpy.types.Object
    for object in bpy.context.view_layer.objects:
        object.select_set(object in objects)

    bpy.context.view_layer.objects.active = objects[0]

    return objects


def magic_uv_clip_and_shift_flipped(objects):

    import bpy
    import bmesh

    bpy.ops.preferences.addon_enable(module='magic_uv')

    mesh_to_objects = list_by_key(objects, lambda x: x.data)

    mesh: bpy.types.Mesh
    for mesh, objects in mesh_to_objects.items():

        select_objects([objects[0]])

        mesh.uv_layers[0].active_clone = True
        mesh.uv_layers[0].active = True
        bake_uv = mesh.uv_layers.new(name = '_new_uvs')
        bake_uv.active = True

        bpy.ops.object.mode_set(mode='EDIT', toggle=False)

        bpy.ops.mesh.reveal()
        bpy.ops.uv.reveal()
        bpy.ops.uv.select_all(action='SELECT')

        bpy.ops.uv.muv_clip_uv(clip_uv_range_max=(0.5, 0.5), clip_uv_range_min=(-0.5, -0.5))

        bpy.ops.uv.select_all(action='DESELECT')
        bpy.ops.uv.muv_select_uv_select_flipped(selection_method='RESET', sync_mesh_selection=False)

        b_mesh = bmesh.from_edit_mesh(mesh)

        uv_layer = b_mesh.loops.layers.uv.verify()

        for face in b_mesh.faces:
            for loop in face.loops:
                if loop[uv_layer].select:
                    loop[uv_layer].uv[1] -= 2
                    loop[uv_layer].uv[0] *= -1

        bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False)

        bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
        
        
import bpy

magic_uv_clip_and_shift_flipped([bpy.data.objects["Geo_Ship_Fed_Fc3_Saucer_Type1_2019"]])

bpy.types.MeshLoop.bitangent_sign is limited to triangles and quads RuntimeError: Error: Tangent space can only be computed for tris/quads, aborting and has an issue with zero aria faces.

https://docs.blender.org/api/current/bpy.types.MeshLoop.html#bpy.types.MeshLoop.bitangent_sign

import bpy

o = bpy.context.object
mesh = o.data

mesh.calc_tangents()


signs = [0.0] * len(mesh.loops)
mesh.loops.foreach_get('bitangent_sign', signs)


uv_layer = mesh.uv_layers.active

for index, sing in enumerate(signs):
    uv_layer.vertex_selection[index].value = sing == -1

mesh.free_tangents()
# [-1. -1. -1. ... -1. -1. -1.]
$\endgroup$
29
  • 1
    $\begingroup$ There is no difference between an island that is flipped by U and V and one rotated by 180 degrees for 2D. tangent_sign or bitangent_sign is used for tangent space normal maps. bitangent = bitangent_sign * cross(normal, tangent)). You can preview tangent_sign values by using the Attribute node in Cycles. Normal maps should not be used with sRGB and it is not related to tangent sign. Also your test_N.dds was not packed into the blend. $\endgroup$
    – unwave
    Commented Jun 10 at 10:51
  • 1
    $\begingroup$ Not clear what your end goal is. Post screenshots of how the result looks in your engine, the target engine and Blender. If the problem is with some of the engines respecting or not respecting the tangent sign — the islands you should care about is the flipped ones only by one axis. And then whether or not you can flip without changing the look depends on the symmetry and directionality of the texture. $\endgroup$
    – unwave
    Commented Jun 10 at 12:32
  • 1
    $\begingroup$ Are you talking only about tangent space normal maps? Tangent sign is usually should not matter, should not be baked into normal maps and is handled at the tangent to world space transformations. If the tangent space normal map was baked for a specific UV map when it should be used with that UV map as it is baked for that specific set of UVs, tangents and normals. Editing the UVs and the texture without re-baking depends on the texture and the model. As a general solution it should be re-baked. The usual tiling normal map is a common case of normals baked for a plane that makes things easier. $\endgroup$
    – unwave
    Commented Jun 11 at 11:01
  • 1
    $\begingroup$ I am making a lot of guessing. It would be better if you share imagery of the model you are trying to convert. $\endgroup$
    – unwave
    Commented Jun 11 at 11:05
  • 1
    $\begingroup$ Do you use github.com/SesamePaste233/StarfieldMeshConverter ? I found a person with a similar issue and the problem was with the exporter and was resolved later in an update. I have an automated solution for re-baking the materials without flipped UVs but the set-up is experimental. $\endgroup$
    – unwave
    Commented Jun 15 at 9:50
2
$\begingroup$

(Using Blender 3.6.12)

Progress Report

(NB: This answer is still under work, and it will most probably evolved with feedbacks).

Objectives

  • To find a diagnostic process identifying faces with flipped UV, discriminating flipped U, flipped V and flipped U&V cases.
  • To attach flipped textures to such faces after correction of UV map.

Approach

To simplify investigations, the outer ring of the hull is isolated as an object. To do so, in Edit Mode, the original object is firstly split in 96 parts using Mesh > Separate > By Loose Parts. Then a face of the outer ring is selected, and the remaining faces are selected using Select > Select Linked > Linked with Delimit UVs setup. Eventually, the outer ring is isolated using Mesh > Separate > Selection. Remaining parts are moved in a collection.
In Shading Editor, a Generated UV Color Grid node is linked as Base Color to the Principled BSDF shader. The provided normal map is uploaded as "NormalMap.png" and it is input in an Image Texture node linked to a Normal Map node. It is to notice that the Color Space is set to sRGB, instead of Non-Color, to match the result achieved with "the other rendering engine".

Shading Editor view

Analysis

UV Editing

1. The top right figure of this picture shows that the UV map of the outer ring is divided in four parts using the same portion of the "UV Color Grid" picture. The U direction is in the "azimuthal" direction whereas the V direction is in the "radial" direction, pointing outward.
2. The left figure shows that the UV map of the outer ring is restricted to a rectangle, outside of the $[0,1]^2$ space. But thanks to the property Extension of the Image Texture nodes set to Repeat, it does not matter.
3. To illustrate how the original UV mapping is folded upon itself, four "symmetric" triangles are selected in the bottom right figure. These are drawn superimposed in the left view.
4. As a conclusion, the NE and SW quarters are rendered as expected with U clockwise, while the NW and SE quarters are showing UV flipped in U direction, U increasing counter-clockwise.


Identifying flipped UV faces

The objective of this section is to identify faces with flipped U or V direction (not U&V).

Outter ring UV colors Outter ring UV flipped

These pictures illustrate that a GeometryNodes modifier, detailed thereafter, can reproduce the same analysis as the Magic UV add-on. Flipped faces are rendered in red, regular faces in blue.

GeometryNodes graph

GN Main Flipped UV faces

(NB: only the four leftmost nodes of the main graph are relevant for this section)
1. By default, the Blue material is set to all faces.
2. The Flipped Face group node is returning a boolean attribute True for faces with flipped U or V direction, without indication of which one.
3. For such faces, the material is changed to Red using a second Set Material node.

GN Flipped UV faces

It is assumed that all faces are triangles. Let $M_0$, $M_1$ and $M_2$ be the three vertices of such a face.
1. The index of $M_0$ is recovered by a Capture Attribute node, set in Face domain, connected to a Corners of Face node. With default settings, it is returning the vertex with the smallest index.
2. Indices of $M_1$ and $M_2$ are recovered from $M_0$ with Offset Corner in Face nodes. (Side note: the bottom Offset Corner in Face node with Offset set to 0 is useless. It is nevertheless introduce to keep the symmetry of the process for the three vertices.)
3. For increasing offsets, the Offset Corner in Face node is providing vertices turning around the face in the direct direction defined by the face normal in XYZ space, labelled $\vec{n}$. Consequently, the cross product $\overrightarrow{M_1M_2} \times \overrightarrow{M_1M_0}$ is always pointing in the same direction as $\vec{n}$ in XYZ coordinate system.
4. The UV mapping of a face is flipped if vertices $M_0$, $M_1$ and $M_2$ are ordered in UV coordinate system in clockwise direction.
5. Let W be the third direction such that UVW defines a direct system. The cross product $\overrightarrow{M_1M_2} \times \overrightarrow{M_1M_0}$ computed in UV coordinate system is aligned with W direction. Consequently, the UV mapping of a face is flipped if the W coordinate of this cross product is negative.
6. The UVMap is stored in an attribute named "Float2". Its values are recovered at vertices $M_0$, $M_1$ and $M_2$ with Sample Index nodes set in Face Corner domain to be connected to Offset Corner in Face nodes.
7. It is to notice that vectors $\overrightarrow{M_1M_2}$ and $\overrightarrow{M_1M_0}$ are Normalized before computing the Cross Product to reduce numerical errors and to make the Less Than 0 comparison less sensitive for highly stretched triangles.
8. Eventually, the "Flipped" criterion computation is triggered by a Capture Attribute node set in Face domain.


Identifying flipped condition

The objective of this section is to determine if U or V coordinate is flipped crossing an edge.

Outter ring UV flipped Outter ring UV delta UV

These pictures illustrate that a GeometryNodes modifier, detailed thereafter, can tag edges at the border of flipped parts or at the border of UV islands, across which the UV mapping is discontinuous.
The top picture shows in yellow the edges detected along such borders for the outer ring.
The bottom picture is an intermediate result showing the variations of U and V along these edges in the Spreadsheet Editor. It is to notice that variations are null in U direction, which is the flipped coordinate in this demonstration case. It is foreseen that such variations could be analysed to assess the flipped coordinate in general.

GeometryNodes graph

GN Main Delta UVs

(An Edge Corners node mimicking the Edge Vertices node is developed to recover the corners on both side of an edge, assuming no more than two faces are connected to a single edge. Then the UVs at those corners are compared.)


Preliminary diagnostic

Full Ship UV colors Full Ship UV flipped

(No correction is proposed for the time being, only a visualisation of flipped faces (in red) and edges with discontinuous UV maps (in yellow). This will be used to determine UV islands later...)

Resources

Blender file with packed texture images:

$\endgroup$
3
  • 1
    $\begingroup$ I am terribly sorry I am running out of time tonight for a more detailed explanation (instead of bits of sentences !). I will improve this asap. Could you at least let us know if this is in the direction (at least for the diagnostic) you are looking for ? $\endgroup$ Commented Jun 17 at 21:53
  • $\begingroup$ I'm just going to say you're definitely on a very similar path to what I've been. And the visuals of flipped UVs certainly appears correct. Unfortunately I've yet to find a functional equation that will identify the direction(s) of the flip. I do want to thank you for actually looking into my problem, and wish you good luck! $\endgroup$ Commented Jun 17 at 22:46
  • $\begingroup$ Your math problem does not make much sense. It is like looking at a coin and wonder is there an equation that can answer which side the coin was facing when it was laying in a hand. The answer is: it does not matter. $\endgroup$
    – unwave
    Commented Jun 20 at 9:18
1
$\begingroup$

Are you sure the problem is even with the UVs and not your normal map? The mesh looks messed up with the normal in your blend file, however when I download the image you posted and apply that it looks completely fine. I'm not familiar with the .dds format but it's not anything I've ever seen in used for a normal map.

This is how it looks once I use the image just saved as a .PNG from the browser.

enter image description here

Node tree, inverted the G channel, making sure the texture is set to Non-Color.

enter image description here

$\endgroup$
6
  • $\begingroup$ See, that's part of the problem. If you set the normal map color space as you should, to non-color, Blender will happily run with it - regardless of any flipped UVs. Unfortunately the target game engine is a lot pickier. In order to get Blender to have a similar result, you have to set the color space to sRGB, then you'll get a similar result. Like I said, you can definitely verify that there are a lot of flipped UVs by selecting said UVs with Magic UV $\endgroup$ Commented Jun 15 at 2:15
  • $\begingroup$ Your original map doesn't work with the same settings. I would check your normal map first before jacking with winding order and recalculating individual face tangents. That makes no sense whatsoever. $\endgroup$
    – Jakemoyo
    Commented Jun 16 at 19:24
  • $\begingroup$ you've literally made an answer proving there's nothing wrong with the normal map. $\endgroup$ Commented Jun 16 at 21:01
  • $\begingroup$ It's not the normal map you're using. It's one I downloaded from this website. It's a png file. Yours is some .dds. Regardless your first thought for fixing this issue should not be manually recalculate the tangent base for each face. There is about 5 other ways to fix this issue I guarantee it that are simpler and more straightforward than that. $\endgroup$
    – Jakemoyo
    Commented Jun 17 at 13:16
  • $\begingroup$ It's the same image - the one I posted on this site is merely the .dds converted to .png in order to appear on the site. But minus any image data lost due to imgur's resampling, it's the same image. I assure you, it's not my first thought. I've been struggling and investigating this issue for over a month. At first I thought it might be that the normal map wasn't tangent space. And after trying to find a way to covert an object space or world space normal to tangent space, I discovered the flipped UVs. Upon experimentation, I discovered that unflipping said UVs would fix my problem $\endgroup$ Commented Jun 17 at 14:18
0
$\begingroup$

So, after breaking down and doing it "manually" (flipping sections of UV islands) and being unable to avoid weird shading artifacts, I broke down and decided to completely redo the UV Unwrapping. Then, using Substance Designer I baked new textures based off the original UVs and textures.

$\endgroup$
2
  • 1
    $\begingroup$ So I was right to not understand the question then. The problem was never where described. $\endgroup$ Commented Jun 27 at 11:29
  • $\begingroup$ To be fair I was still struggling to discover the entire problem- and what was described certainly was part of the problem, it's just that it also included UVs that weren't flipped, but still poorly oriented. I realize that "poorly oriented" isn't very descriptive, but i'm still struggling on determining what was causing the normal related issues. And seeing as how doing a fresh unwrap solved the issue, it was definitely UV related. I just wish there was a way to solve it without starting from scratch $\endgroup$ Commented Jun 27 at 12:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .